Integration guide

Auth System

Full auth for Next.js App Router: email/password with bcrypt, GitHub and Google OAuth, email verification, brute-force lockout, JWT sessions, and role-based access control. One file. One Supabase migration.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the Auth System block, and click Copy all files. Paste lib/auth.ts (~620 lines) into your project. The SQL migration is embedded as the MIGRATION constant at the top of the file.

2. Prerequisites

  • Next.js 14 or 15, App Router
  • Node 18+ or Bun
  • A Supabase project (free tier is sufficient)
  • GitHub OAuth App and/or Google OAuth credentials for social login
  • An SMTP provider for email verification (Postmark, Resend, AWS SES)

3. Install

  1. Copy auth.ts from the block detail page and paste it into lib/auth.ts in your project.
  2. Install peer dependencies:
bash
npm install next-auth @supabase/supabase-js bcryptjs nodemailer zod
npm install -D @types/bcryptjs @types/nodemailer

4. Environment

VariableRequiredDefaultPurpose
NEXTAUTH_URLyesCanonical URL of your app, e.g. https://example.com
NEXTAUTH_SECRETyesRandom 32-byte base64 string. Generate: openssl rand -base64 32
NEXT_PUBLIC_SUPABASE_URLyesYour Supabase project URL (Settings → API)
SUPABASE_SERVICE_ROLE_KEYyesSupabase service role key — server-side only
GITHUB_CLIENT_IDnoGitHub OAuth App client ID. Omit to disable GitHub sign-in
GITHUB_CLIENT_SECRETnoGitHub OAuth App client secret
GOOGLE_CLIENT_IDnoGoogle OAuth client ID. Omit to disable Google sign-in
GOOGLE_CLIENT_SECRETnoGoogle OAuth client secret
SMTP_HOSTyesSMTP server hostname for email verification
SMTP_PORTyes587SMTP port (587 for TLS, 465 for SSL)
SMTP_USERyesSMTP username or API token
SMTP_PASSyesSMTP password or API token
EMAIL_FROMyesFrom address: noreply@yourdomain.com

5. Database

The MIGRATION constant at the top of auth.ts contains all SQL. Run it in Supabase SQL Editor (Dashboard → SQL Editor → New query → paste → run).

Creates: profiles, auth_tokens, and auth_events tables with RLS enabled. The service role key bypasses RLS; client-side (anon) access is blocked on all three tables.

6. Wire it in

API route:

app/api/auth/[...nextauth]/route.ts
import { authOptions } from '@/lib/auth'
import NextAuth from 'next-auth'

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

Read session server-side:

app/protected/page.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function Page() {
  const session = await getServerSession(authOptions)
  if (!session) redirect('/api/auth/signin')
  return <div>Signed in as {session.user.email}</div>
}

Route protection via middleware:

middleware.ts
export { default } from 'next-auth/middleware'
export const config = { matcher: ['/dashboard/:path*'] }

7. Verify it works

  1. Navigate to /api/auth/signin. The sign-in page should render.
  2. Register with email and password. Confirm a verification email arrives.
  3. Click the verification link. Check profiles.email_verified_at is set in Supabase.
  4. Sign out and sign back in. Confirm auth_events has a new sign_in row.
  5. Fail login 5 times. Confirm profiles.locked_until is set and the error "Account locked" appears.

8. Failure modes & fixes

Error: NEXTAUTH_SECRET is not set

Cause: NextAuth requires a secret in all non-development environments.

Fix: Set NEXTAUTH_SECRET in .env.local. Generate: openssl rand -base64 32

OAuthCallbackError: Invalid state

Cause: The OAuth callback URL in your GitHub/Google App settings does not match NEXTAUTH_URL.

Fix: Set callback URL to https://your-domain.com/api/auth/callback/github (or /google) in the OAuth app console.

Could not find service role key

Cause: The Supabase admin client is reading SUPABASE_SERVICE_ROLE_KEY, which is not set.

Fix: Add SUPABASE_SERVICE_ROLE_KEY (Supabase → Settings → API → service_role). Not the anon key.

Verification email not arriving

Cause: SMTP credentials incorrect, or sending domain lacks SPF/DKIM.

Fix: Check SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS against your provider's docs. Add SPF/DKIM records for your sending domain.

AuthorizationError: Account locked

Cause: 5 consecutive failed login attempts trigger a 15-minute lockout.

Fix: Wait 15 minutes, or clear profiles.locked_until in Supabase SQL Editor for the affected row.

PostgreSQL RLS error on insert

Cause: Using the anon key instead of the service role key for a write operation.

Fix: Confirm SUPABASE_SERVICE_ROLE_KEY is set and that auth.ts uses it — not NEXT_PUBLIC_SUPABASE_ANON_KEY.

9. Security responsibilities

What this block handles:

  • bcrypt hashing at cost 12. Plaintext passwords are never stored.
  • Email verification tokens: single-use, 24-hour TTL.
  • Password reset tokens: single-use, 15-minute TTL.
  • Brute-force lockout: 5 failures → 15-minute lock.
  • Auth event logging: sign_in, sign_out, register, password_reset.
  • JWT sessions (30-day TTL). No session state in the database.

What you must ensure:

  • HTTPS in production. Session cookies are not protected over plaintext HTTP.
  • Keep NEXTAUTH_SECRET and SUPABASE_SERVICE_ROLE_KEY out of version control and client bundles.
  • Set NEXTAUTH_URL to your exact production URL. OAuth callbacks fail if this is wrong.
  • Rotate NEXTAUTH_SECRET if you believe it was leaked — this invalidates all active sessions.