| [ Web Proxy ] |
| Viewing: https://marrowstack.dev/docs/blocks/auth | [Back] [Original] |
Integration guide
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.
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.
auth.ts from the block detail page and paste it into lib/auth.ts in your project.npm install next-auth @supabase/supabase-js bcryptjs nodemailer zod
npm install -D @types/bcryptjs @types/nodemailer| 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 |
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.
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*'] }/api/auth/signin. The sign-in page should render.profiles.email_verified_at is set in Supabase.auth_events has a new sign_in row.profiles.locked_until is set and the error "Account locked" appears.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.
What this block handles:
What you must ensure:
| Web Proxy Viewer | New URL | Original Page |