Skip to content
Chapter 52Lesson 5

Quiz - Better Auth setup

Quiz progress

0 / 0

A teammate’s sign-up Server Action runs cleanly — it returns a 200, and the user and session rows appear in Postgres — but the user is never actually signed in: the next request is anonymous again. Their auth.ts is below.

export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg', schema }),
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
});

What’s the fix?

Add the nextCookies() plugin (plugins: [nextCookies()]). Without it, the Set-Cookie header an action emits never attaches to the action’s response, so the session cookie is created server-side and silently dropped — the rows exist, but the browser never gets the cookie.

The session rows shouldn’t be written by a Server Action at all — move the signUp call into the catch-all route handler so the cookie is set on a normal HTTP response.

baseURL is being read from validated env instead of letting Better Auth auto-detect the request origin, so the cookie’s Domain is wrong — drop baseURL and the cookie will attach.

You need to read the current user in each of these places. Pick the two that should call authClient.* rather than the server-side auth.api.*.

A sign-in form’s submit handler inside a 'use client' component.
A header avatar in a Client Component that shows who’s signed in.
proxy.ts deciding whether to bounce a signed-out visitor.
A Server Action reading the current user before a mutation.

Better Auth stores no password column on the user table — the password hash lives on account instead, and it’s nullable. Why is that the right decomposition?

A user is one identity; each account row is one way to prove it. A password is one proof method (alongside Google, GitHub, …), so it belongs on the proof row and is null for OAuth-only accounts. “Change password” updates one account row, “link Google” inserts one — and the user row never moves.

Hashes are large, so splitting them onto account keeps the user table narrow and faster to scan — it’s a storage-layout optimization, and the column is nullable only to save space on OAuth rows.

The password lives on account so it can cascade-delete independently of the user; making it nullable lets a user temporarily detach their password without deleting their identity.

Better Auth ships its own npx @better-auth/cli migrate that can create the four tables directly. This stack never uses it — generate the schema with the CLI, but migrate with Drizzle Kit, always. What breaks if you let both tools touch the database?

You’d have two tools writing schema with two separate, conflicting ideas of the database’s current state — so the migration history lies and the schema drifts out of sync with what’s checked in. One generator, one migrator, no overlap.

Better Auth’s migrate skips the unique index on session.token, so the per-request session lookup loses its index and the hot path slows down.

The two tools hash passwords differently, so credential accounts created before the switch can no longer sign in.

A user signed in this morning and has been clicking around the app all afternoon. When they click “change password,” the action re-prompts for their password even though they’re plainly active. With freshAge set to 10 minutes, why?

freshAge is measured from when the session was created — sign-in this morning — not from the last activity. The session is well past its 10-minute fresh window, so the destructive action demands a re-authentication regardless of how recently the user clicked something.

The session crossed its updateAge boundary, which expired the fresh window; any click after updateAge forces a re-prompt on sensitive actions until the session is renewed.

The expiresIn wall is approaching, and Better Auth re-prompts for the password on sensitive actions once the session is within freshAge of absolute expiry.

The course configures the production session cookie with cookiePrefix: '__Host-better-auth'. What does naming the cookie with the __Host- prefix actually buy you?

The browser refuses to store the cookie unless it’s Secure, has Path=/, and carries no Domain — turning tight scoping from something you must remember into something the platform enforces. It’s a browser contract, not a Better Auth feature.

Better Auth signs the cookie value with a host-bound key, so the opaque token can only be replayed from the same host that issued it.

It enables cross-subdomain session sharing, so one login works across app.example.com and admin.example.com without extra config.

To dedupe the per-request session read, a developer wraps it like this:

const getSession = async () => {
'use cache';
return auth.api.getSession({ headers: await headers() });
};

What’s wrong with it?

'use cache' persists across requests and across users, and its key is built from arguments and captured values — the cookie isn’t one of them. So user B can be handed the entry computed for user A: an account-takeover bug. Per-request session reads belong in React.cache, which is request-scoped and discarded when the request ends.

'use cache' can’t wrap an async function, so the read never resolves — switching to React.cache is required only because it supports Promises.

Nothing is functionally wrong, but 'use cache' is slower than React.cache for per-request work, so it’s just a performance regression.

proxy.ts gates /dashboard by calling getSessionCookie(request, { cookiePrefix: SESSION_COOKIE_PREFIX }) and bouncing when no cookie is found — it never calls auth.api.getSession to validate the session, even though the Node-runtime proxy could. Why is that the deliberate, correct design?

The proxy is an optimistic, cheap presence check — a bouncer confirming you’re holding a ticket, not that it’s genuine. Real validation and every authorization decision happen later against the database (e.g. requireUser() in the layout), which also avoids trusting the cookie cache, which can be stale for minutes after a revocation.

The proxy runs on the edge runtime, which can’t reach Postgres, so a full getSession read is impossible there — cookie-presence is the only option available.

A presence check is more secure because it can detect a forged cookie that auth.api.getSession would mistakenly accept as valid.

Quiz complete

Score by topic