set-up-nextjs-supabase-auth
Skill7 starsChanged 16 days agoMITView on GitHub
Use when adding or fixing Supabase Auth in a Next.js 16 App Router app — @supabase/ssr cookie clients, session refresh in proxy.ts, verified getClaims() on the server, protected routes, PKCE callback and email-link routes, and sign-in by password, magic link or OAuth.
- Reads credentials
- Installs packages
- Sends data out
1 other project uses a version of this.
Adopted by
Most recently taken up by
- VelimirMueller/claude-skillsword for word, first seen Oct 11, 2026
Other versions1 project uses a version of this.
Word for word · 1
- VelimirMueller/claude-skills★ 7100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository156 other files it gives its agents.
- skills/swift/swiftui-design-principles/AGENTS.mdAGENTS.md
- skills/swift/ios-code-audit/CLAUDE.mdCLAUDE.md
- skills/swift/swift-focusengine-pro/llms.txtllms.txt
- build-llm-seamSkill
- build-mcp-serverSkill
- build-rag-pipelineSkill
- secure-llm-featuresSkill
- set-up-llm-evalsSkill
- build-nextjs-backendSkill
- build-supabase-edge-functionSkill
- configure-backend-testsSkill
- design-http-apiSkill
- harden-backendSkill
- scaffold-fastapi-serviceSkill
- scaffold-go-serviceSkill
- scaffold-hono-serviceSkill
- secure-supabase-rlsSkill
- set-up-backend-authSkill
- set-up-background-jobsSkill
- set-up-databaseSkill
- set-up-observabilitySkill
- set-up-supabaseSkill
- build-cliSkill
- release-cliSkill
- set-up-dev-toolchainSkill
- audit-securitySkill
- audit-toolchainSkill
- extend-skillsetSkill
- promote-releaseSkill
- set-up-stack-profileSkill
- write-commit-messagesSkill
- write-pull-requestsSkill
- analyze-complexitySkill
- apply-algorithmsSkill
- apply-design-patternsSkill
- apply-design-principlesSkill
- apply-solidSkill
- choose-data-structureSkill
- debug-systematicallySkill
- enforce-dependency-ruleSkill
- refactor-safelySkill
- slice-and-estimateSkill
- write-clean-testsSkill
- write-readable-codeSkill
- clean-frontend-scaffoldingSkill
- configure-accessibilitySkill
- configure-analyticsSkill
- configure-ciSkill
- configure-error-trackingSkill
- configure-lintingSkill
- configure-test-stackSkill
- configure-typescriptSkill
- create-moduleSkill
- optimize-performanceSkill
- scaffold-frontend-projectSkill
- set-up-authSkill
- set-up-design-systemSkill
- set-up-document-headSkill
- set-up-error-boundariesSkill
- set-up-feature-flagsSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 12
Read .claude/stack-profile.md (frontend.meta: next, backend.track).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Set Up Next.js + Supabase Auth
The session lives in httpOnly cookies, is refreshed in proxy.ts, and is verified on the server with getClaims(). Auth is server-side only: Server Actions and route handlers sign in, sign out and exchange the PKCE code, so JavaScript never reads a token. Redirects are UX; Postgres RLS is the authorization (secure-supabase-rls).
1. Audit current state
Read .claude/stack-profile.md (frontend.meta: next, backend.track). Then:
grep -n '"next"\|@supabase' package.json; ls middleware.ts src/middleware.ts proxy.ts src/proxy.ts 2>/dev/null
grep -rn "auth-helpers\|createClientComponentClient\|createServerComponentClient" src 2>/dev/null # deprecated package
grep -rn "getSession()" src --include=*.ts --include=*.tsx | grep -v "libs/supabase" # server use = trusting the cookie
grep -rn "createClient(" src | grep -v "src/libs/supabase" # clients outside the seam
ls src/app/auth src/libs/supabase src/server/auth.ts 2>/dev/null
grep -n "site_url\|additional_redirect_urls\|\[auth.email.template" supabase/config.tomlFindings: getSession() or getUser() results trusted for authorization without verification, a Supabase client in a module variable, middleware.ts on Next 16, the secret key in an auth path, an unvalidated next redirect.
2. Decide what to do
- No
@supabase/ssr→ full setup (steps 4–8). - Clients exist, no proxy refresh → add the proxy (step 6); random sign-outs follow without it.
getSession()on the server → replace withgetViewer()(step 5).- Everything present and the checks in step 9 pass → "already in place".
3. Detect the sign-in methods
From existing code and supabase/config.toml ([auth.external.*], [auth.email]). If nothing says which methods the app needs, ask one question: password, magic link, OAuth provider, or a mix. Each is a short block in auth-flows.md; install none you do not need. Next ≤ 15: the file is middleware.ts and the export middleware; the body is identical.
4. Install only what's missing
pnpm add @supabase/supabase-js @supabase/ssr server-only zodEnv (public prefix is fine: both values are public by design) and the validated env seam: set-up-supabase, nextjs-backend-patterns.md "Env". Add NEXT_PUBLIC_SITE_URL (the canonical origin): redirects are built from it, never from the Host header.
5. Generate the seams
src/libs/supabase/admin.ts: as in set-up-supabase. There is no browser client for auth — sign-in and sign-out are Server Actions. The forced cookie options, and the per-request server client:
// src/libs/supabase/cookies.ts
import type { CookieOptions } from '@supabase/ssr';
/** Forced on every cookie the server client writes. Session cookies are httpOnly; see supabase-auth-patterns.md. */
export const AUTH_COOKIE_OPTIONS: CookieOptions = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
};// src/libs/supabase/server.ts
import 'server-only';
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
import { clientEnv } from '@/libs/env.client';
import type { Database } from './database.types';
import { AUTH_COOKIE_OPTIONS } from './cookies';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
clientEnv.NEXT_PUBLIC_SUPABASE_URL,
clientEnv.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
{
cookieOptions: AUTH_COOKIE_OPTIONS,
cookies: {
getAll: () => cookieStore.getAll(),
setAll(cookiesToSet) {
try {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, { ...options, ...AUTH_COOKIE_OPTIONS });
}
} catch {
// Called from a Server Component, which cannot write cookies.
// Safe to ignore: proxy.ts refreshes the session on every request.
}
},
},
},
);
}The verified viewer, used by every DAL function and action:
// src/server/auth.ts
import 'server-only';
import { cache } from 'react';
import { redirect } from 'next/navigation';
import { createClient } from '@/libs/supabase/server';
export type Viewer = { id: string; email: string | null };
export const getViewer = cache(async (): Promise<Viewer | null> => {
const supabase = await createClient();
const { data, error } = await supabase.auth.getClaims(); // verifies the signature on every call
if (error || !data) return null;
const { sub, email } = data.claims;
return { id: sub, email: typeof email === 'string' ? email : null };
});
export async function requireViewer(): Promise<Viewer> {
const viewer = await getViewer();
if (!viewer) redirect('/login');
return viewer;
}getClaims() for identity, getUser() when a revoked session must be caught (password change, payments, account deletion), never getSession() on the server: supabase-auth-patterns.md.
6. Wire the proxy, routes and sign-in
src/proxy.tsplussrc/libs/supabase/proxy.ts(updateSession): refreshes the session, forwards the new cookies to the request and the response, redirects anonymous users fromPROTECTED_PREFIXESto/login. Code and the five rules for it: auth-flows.md, "Proxy".src/app/auth/callback/route.ts: OAuth PKCE code exchange.src/app/auth/confirm/route.ts: email-linktoken_hashverification. Both redirect to a configured origin and to a sanitizednext.src/features/auth/actions.ts:signInWithPasswordAction,signInWithMagicLinkAction,signInWithOAuthAction,signOutAction(scope: 'local'). Login form:src/features/auth/LoginForm.tsx.supabase/config.toml:site_url,additional_redirect_urls(exacthttps://<app>/auth/callbackin production,http://localhost:3000/**for dev), themagic_linktemplate pointing at/auth/confirm, provider secrets viaenv().
7. Protect routes in three layers
proxy.ts: optimistic redirect for anonymous visitors (fast, UX only).- The DAL:
requireViewer()at the top of every data function,getViewer()at the top of every action (build-nextjs-backend). - Postgres RLS: the policy that decides which rows the verified user may touch.
A page needs no check of its own beyond reading through the DAL. With Cache Components, the component that calls getViewer() sits behind <Suspense> and is never inside 'use cache'.
8. Test the pure parts
Unit-test safeNextPath (open-redirect cases) and the action validation; sign-in itself is covered end to end against the local stack. Test file: auth-flows.md, "Tests".
9. Verify
typecheck is tsc --noEmit in a Next.js app (one tsconfig); add "typecheck": "tsc --noEmit" to package.json if it is missing.
pnpm typecheck && pnpm build
pnpm supabase start # local Auth + a local mail catcher (URL printed by `start`)
curl -si http://localhost:3000/notes | head -3 # expect 307 → /login?next=%2Fnotes
curl -si 'http://localhost:3000/auth/callback?code=bad' | grep -i location # expect …/login?error=auth
curl -si 'http://localhost:3000/auth/confirm?token_hash=x&type=bogus' | grep -i location # expect …/login?error=linkScripted end-to-end check with a real session (local stack; the admin call needs the secret key from supabase status): create a user, mint a magic-link token, hit the confirm route, then call a protected page with the cookie jar.
API=http://127.0.0.1:54321
curl -s -X POST $API/auth/v1/admin/users -H "apikey: $SECRET" -H "Authorization: Bearer $SECRET" \
-H 'content-type: application/json' -d '{"email":"owner@example.com","email_confirm":true}' >/dev/null
TH=$(curl -s -X POST $API/auth/v1/admin/generate_link -H "apikey: $SECRET" -H "Authorization: Bearer $SECRET" \
-H 'content-type: application/json' -d '{"type":"magiclink","email":"owner@example.com"}' | jq -r .hashed_token)
curl -si -c jar.txt "http://localhost:3000/auth/confirm?token_hash=$TH&type=magiclink" > confirm.txt
grep -i '^location\|^set-cookie' confirm.txt | cut -c1-80
grep -i '^set-cookie' confirm.txt | grep -q 'sb-.*HttpOnly' && echo "HttpOnly OK"
curl -s -o /dev/null -w '%{http_code}\n' -b jar.txt http://localhost:3000/notes # expect 200, not 307Expected: a redirect to /, a set-cookie: sb-…-auth-token=…; HttpOnly; SameSite=Lax, then 200 on /notes (the proxy and getViewer() accepted the session; RLS filtered the data). Every sb-* cookie carries HttpOnly — the grep above fails loudly otherwise. This ran against Next 16.4.0 and CLI 2.120.0. Then sign in with each enabled method in a browser: the page behind the login loads, DevTools shows sb-<project-ref>-auth-token cookies marked HttpOnly, a reload keeps the session, sign-out clears it and /notes redirects again. Force a token refresh (set jwt_expiry = 60 locally, wait two minutes): the user stays signed in.
References
- supabase-auth-patterns.md —
getClaimsvsgetUservsgetSession, refresh, PKCE, redirects, cookies, sign-out. - auth-flows.md — proxy, routes, actions, form, email template, tests (code).
- ../build-nextjs-backend/SKILL.md, ../secure-supabase-rls/SKILL.md, ../set-up-supabase/SKILL.md.
- ../../frontend/set-up-auth/SKILL.md — the SPA variant (tokens, guards); this skill is its server-rendered counterpart.
- ../../core/_shared/security-baseline.md — rules 3 and 4.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

