raxel.studio

Docs / Authentication

Authentication

Auth is handled by better-auth with the passkey plugin enabled. Email/password is the fallback; passkey is preferred when the browser supports it.

Setup #

The auth instance lives in lib/auth.ts. It's imported by both server and client; the server-only and client-only helpers live in lib/auth-server.ts and lib/auth-client.ts respectively.

You need two env vars:

BETTER_AUTH_SECRET=<32 bytes of random e.g. `openssl rand -hex 32`>
BETTER_AUTH_URL=http://localhost:3000   # in dev

In production, set BETTER_AUTH_URL to your canonical site URL.

The sign-in flow #

The starter ships with a working /sign-in page. It detects whether the browser supports WebAuthn and presents passkey as the primary option; email/password is shown as a secondary path.

  • New user: enters email → starter sends a verification link → user clicks → registers a passkey.
  • Returning user: clicks "Sign in with passkey" → the browser handles the WebAuthn ceremony → session established.
  • Email/password fallback: standard form, no surprises.

Sessions are stored in the database (the session table). The session cookie is HTTP-only and Secure in production.

Protecting a route #

Every page or layout under app/studio/ already has session protection through app/studio/layout.tsx. To gate a new route elsewhere, call the server-side helper:

import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth-server";
 
export default async function GatedPage() {
  const session = await getSession();
  if (!session) redirect("/sign-in");
  // ...
}

Reading the session in a server component #

import { getSession } from "@/lib/auth-server";
 
const session = await getSession();
// session?.user contains { id, email, name, ... }

Adding a sign-out button #

Use the client helper:

"use client";
import { signOut } from "@/lib/auth-client";
 
<button onClick={() => signOut()}>Sign out</button>

signOut() clears the session and refreshes the route.

Email verification and password reset #

Both flows use Resend (optional dependency). If you don't set RESEND_API_KEY, the starter falls back to logging the email content to the console — useful in development.

Roles and authorization #

The starter ships with authentication only. There are no built-in roles or permission models. Add them when you have a concrete authorization story; don't bolt on an RBAC system before you need one.