This guide walks through setting up Supabase authentication in a Next.js 16 App Router project deployed on Vercel. It covers the full stack: database schema with Row Level Security, four Supabase client variants, the proxy layer, server actions, and caching — with security pitfalls called out at each step.
The recommendations here are based on a security audit we ran against publicly known CVEs and advisories through February 2026. Links to sources are inline throughout.
Prerequisites
- Next.js 16.1.6+ (App Router). Patches for all critical CVEs through January 2026 are included.
- React 19.2.3+. The React2Shell RCE (CVE-2025-55182) was patched in React 19.2.2.
- Supabase project with the new key model (
sb_publishable_/sb_secret_). - Vercel deployment (or self-hosted with
next start).
Environment Variables
Any environment variable prefixed with NEXT_PUBLIC_ is bundled into client-side JavaScript. The publishable key is safe to expose — RLS is what protects your data. The SUPABASE_SERVICE_ROLE_KEY must never have the NEXT_PUBLIC_ prefix. This key bypasses all RLS.
Footgun: Preview deployments can receive production credentials via the Supabase Vercel integration. Audit your project settings.
Database Schema & Row Level Security
When you create a table in Supabase, RLS is off. The table is fully accessible to anyone with the publishable key. 83% of exposed Supabase databases involve RLS misconfigurations. CVE-2025-48757 affected 170+ applications, exposing 13,000 users including password reset tokens.
Rule: Enable RLS on every table, no exceptions.
Admin Allowlist Pattern
Rather than granting write access to all authenticated users (auth.role() = 'authenticated'), create an explicit admin allowlist table. auth.role() is deprecated — the recommended replacement is (auth.jwt() ->> 'role').
The is_admin() Helper Function
Place it in a private schema so it's not exposed via the Data API. Always set search_path = '' on SECURITY DEFINER functions to prevent search path injection. Never create SECURITY DEFINER functions in the public schema.
Footgun:user_metadatais user-modifiable. Never use it in RLS policies. Useraw_app_meta_data(server-side only) or a separate table.
Supabase Client Setup (Four Variants)
You need different clients for different contexts: browser (publishable key only), server (publishable + user cookies), static (publishable, no cookies, for generateStaticParams), and middleware (publishable + request cookies for session refresh).
The httpOnly: false Tradeoff
@supabase/ssr intentionally sets cookies without httpOnly. The Supabase client SDK needs JavaScript access for token refresh. Without httpOnly, cookies are vulnerable to XSS — if an attacker injects JS, they can steal the session token.
Mitigations: Set Secure and SameSite=Lax flags. Implement CSP headers. Sanitize all user-provided content.
The Proxy Layer (formerly Middleware)
Next.js 16 renamed middleware.ts to proxy.ts and moved execution to the Node.js runtime. This was partly motivated by CVE-2025-29927 (CVSS 9.1), which demonstrated that middleware could be bypassed entirely via the x-middleware-subrequest header.
The proxy is a UX convenience, not a security boundary. Always verify auth again in server actions and the data access layer.
Server Actions with Auth Guards
Every function marked with 'use server' is a public HTTP endpoint. Anyone can call it by sending a POST with the next-action header. The React2Shell vulnerability (CVE-2025-66478) exploited exactly this surface.
Call requireAuth() at the top of every write action. Validate all inputs — slug format, UUID format, lengths, URL schemes. Auth is checked in three places: proxy (UX), server action (application), database RLS (data). If any one layer fails, the other two protect the data.
File Uploads
Always validate file type against an allowlist, enforce a size limit, and pass contentType explicitly to prevent MIME spoofing. An attacker could upload an HTML file disguised as an image, enabling stored XSS from your storage bucket.
Caching with 'use cache'
Use the static client (no cookies) for cached queries. This ensures only public data enters the cache. Never use 'use cache' on functions that read cookies() or headers(). CVE-2025-57752 demonstrated that authenticated responses cached without user identity in the key leak to other users.
Revalidation
Next.js 16 requires a cache life profile as the second argument to revalidateTag. Always guard against an empty REVALIDATION_SECRET — if unset and you only compare against the header, both sides are undefined and the check passes.
JSON-LD and XSS Prevention
Without .replace(/</g, '\\u003c'), a data field containing </script> breaks out of the JSON-LD script block and executes arbitrary JavaScript. This was reported to the Next.js team and the official docs were updated.
Always escape < in JSON-LD output.
Security Audit Checklist
Database: RLS on every table. No SECURITY DEFINER in public schema. All have set search_path = ''. Admin via allowlist table. No user_metadata in RLS.
Environment: No NEXT_PUBLIC_ on service key. REVALIDATION_SECRET set. Preview envs isolated. .env.local in .gitignore.
Application: requireAuth() on every write action. Input validation everywhere. File uploads checked. 'use cache' only with static client. Defense in depth. JSON-LD sanitized.
Dependencies: Next.js 16.0.7+ (React2Shell). Next.js 16.0.10+ (DoS + source exposure). React 19.2.2+.
CVE Reference
CVE-2025-66478 (Critical 10.0): React2Shell — RSC deserialization RCE. Fixed in Next.js 16.0.7.
CVE-2025-55182 (Critical 10.0): Upstream React variant. Fixed in React 19.2.2.
CVE-2025-29927 (Critical 9.1): Middleware bypass via x-middleware-subrequest. Fixed in Next.js 15.2.3.
CVE-2025-55184 (High): DoS via infinite loop. Fixed in Next.js 16.0.10.
CVE-2025-57752 (High): Image cache confusion. Fixed in Next.js 15.4.5.
CVE-2025-55183 (Medium): Source code exposure. Fixed in Next.js 16.0.10.
CVE-2025-49005 (Medium): Cache poisoning via missing Vary header. Fixed in Next.js 15.3.3.
CVE-2025-48757 (High): 170+ apps with missing Supabase RLS. 13,000 users exposed.