Skip to Content
LaunchfastDocs

Authentication

How sign-in works (email and password, Google, GitHub), how to read the signed-in user, and how to protect a page.

Sign-in is built on Better Auth and configured in src/lib/auth.ts. Users, sessions and accounts are stored in your Postgres database.

Sign-in methods

MethodTurns on whenNotes
Email and passwordAlways in development. In production, only when RESEND_API_KEY is setEmail must be confirmed before the first sign-in. Minimum 8 characters
GoogleGOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are setSee Google and GitHub sign-in
GitHubGITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are setSame

Why email sign-in disappears in production

Email sign-in needs to send confirmation and password reset emails. In production without Resend, those emails can't be sent, so the form is hidden and only social sign-in is offered. Set up Resend to bring it back.

The email and password flow

  1. Sign up. The user enters name, email and password. A confirmation email is sent. Locally without Resend, the link prints in your npm run dev terminal, and the sign-up page says so.
  2. Confirm. Clicking the link confirms the email and signs the user in.
  3. Sign in. If the email isn't confirmed yet, sign-in fails with "Please confirm your email first. We sent you a new link." and a new link is sent.
  4. Forgot password. /forgot-password sends a reset link. Resetting the password signs the user out on every other device.

Signing up again with an email that already has an account shows the same screen as a new sign-up (so nobody can find out which emails have accounts), and the owner of that email gets a message telling them they already have an account.

Social sign-in and existing accounts

  • Same email, different method: someone who signed up with email and password can later use "Continue with Google" with the same email. Both sign into one account.
  • Unconfirmed email account: if someone signed up with an email but never confirmed it, then uses Google or GitHub with that email (which the provider has verified), they're signed in and the unconfirmed password is removed. That stops a stranger who registered someone else's email first from keeping a way in.
  • Provider didn't verify the email: sign-in is refused with account_not_linked. See Sign-in errors.

Rate limiting

Better Auth's rate limits are on, and stored in Postgres so they hold across all of Vercel's server instances. Too many attempts return 429 Too Many Requests for a short while.

Reading the signed-in user

Use the helpers in src/lib/session.ts. Never read cookies yourself.

src/app/dashboard/settings/page.tsx
import { Suspense } from 'react';
import { requireUser } from '@/lib/session';

export default function SettingsPage() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Settings />
    </Suspense>
  );
}

async function Settings() {
  // Redirects to /sign-in when nobody is signed in.
  const user = await requireUser();
  return <p>Signed in as {user.email}</p>;
}
HelperReturns
getSession()The session (with .user), or null
requireUser()The user, or redirects to /sign-in
requireAdmin() (in src/lib/admin.ts)The admin user, or a 404 page for everyone else

Read the session inside Suspense

The kit uses Next.js 16 with cacheComponents: true. Reading the session, headers(), cookies() or searchParams must happen inside a component wrapped in <Suspense>, as above. Awaiting them at the top of a page fails the build. See Build errors.

Protecting pages

Each page checks the user itself with requireUser() or requireAdmin(). src/proxy.ts also redirects visitors without a session cookie away from /dashboard and /admin, but only to save a round trip: it doesn't check that the session is valid, so it's not a security check.

Add new signed-in pages under src/app/dashboard/.

Signing in on the client

'use client';
import { authClient } from '@/lib/auth-client';

await authClient.signIn.social({ provider: 'google', callbackURL: '/dashboard' });
await authClient.signOut();

The auth secret

BETTER_AUTH_SECRET signs session cookies. Changing it signs everyone out. Use a different value in each project, and never commit it.

On this page