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
- Copy
auth.tsfrom the block detail page and paste it intolib/auth.tsin your project. - Install peer dependencies:
npm install next-auth @supabase/supabase-js bcryptjs nodemailer zod
npm install -D @types/bcryptjs @types/nodemailer4. Environment
| Variable | Required | Default | Purpose |
|---|---|---|---|
| NEXTAUTH_URL | yes | — | Canonical URL of your app, e.g. https://example.com |
| NEXTAUTH_SECRET | yes | — | Random 32-byte base64 string. Generate: openssl rand -base64 32 |
| NEXT_PUBLIC_SUPABASE_URL | yes | — | Your Supabase project URL (Settings → API) |
| SUPABASE_SERVICE_ROLE_KEY | yes | — | Supabase service role key — server-side only |
| GITHUB_CLIENT_ID | no | — | GitHub OAuth App client ID. Omit to disable GitHub sign-in |
| GITHUB_CLIENT_SECRET | no | — | GitHub OAuth App client secret |
| GOOGLE_CLIENT_ID | no | — | Google OAuth client ID. Omit to disable Google sign-in |
| GOOGLE_CLIENT_SECRET | no | — | Google OAuth client secret |
| SMTP_HOST | yes | — | SMTP server hostname for email verification |
| SMTP_PORT | yes | 587 | SMTP port (587 for TLS, 465 for SSL) |
| SMTP_USER | yes | — | SMTP username or API token |
| SMTP_PASS | yes | — | SMTP password or API token |
| EMAIL_FROM | yes | — | From 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:
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:
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:
export { default } from 'next-auth/middleware'
export const config = { matcher: ['/dashboard/:path*'] }7. Verify it works
- Navigate to
/api/auth/signin. The sign-in page should render. - Register with email and password. Confirm a verification email arrives.
- Click the verification link. Check
profiles.email_verified_atis set in Supabase. - Sign out and sign back in. Confirm
auth_eventshas a newsign_inrow. - Fail login 5 times. Confirm
profiles.locked_untilis 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.