Running Next.js on Supabase Securely
DevOps runbook for Next.js on Supabase: architecture, middleware, auth patterns, rate limiting, and Claude Code integration.
Once Supabase is running reliably as a self-hosted backend platform, the actual application stack is built on top of it.
In many modern projects, Next.js takes on the role of the app layer:
- Frontend Rendering
- Server Side Rendering
- Server Actions
- Route Handlers
- API Proxy
- Session Handling
This makes Next.js effectively a backend gateway between browser and Supabase.
The most common mistake is treating this layer as a “frontend” when it actually contains server-side logic with elevated privileges.
This runbook describes how to run Next.js securely on top of a self-hosted Supabase platform.
At a Glance - Part 2 of 6 in the DevOps Runbook Series
- Next.js runs as a separate container, physically isolated from Supabase
- service_role key allowed in exactly one file only (lib/supabase/admin.ts)
- Middleware with getUser() on every request (not getSession())
- Ownership checks in all server actions before data mutations
- Rate limiting with Upstash Redis on all auth endpoints
Each section contains:
- Implementation
- Verifiable condition
- Failure scenario
Series Table of Contents
This guide is part of our DevOps runbook series for self-hosted app stacks.
- Supabase Self-Hosting Runbook
- Running Next.js on Supabase Securely - this article
- Deploying Supabase Edge Functions Securely
- Running Trigger.dev Background Jobs Securely
- Claude Code as Security Control in DevOps Workflows
- Security Baseline for the Entire Stack
Article 1 describes the platform foundation. This article covers the app layer on top.
Architecture Overview
Browser (Client)
|
| HTTPS
|
Next.js App Layer
|
+-- @supabase/ssr
|
+-- service_role client
(isolated admin contexts only)
|
Supabase Platform Layer
|
+-- Kong API Gateway
+-- GoTrue Auth
+-- PostgREST API
+-- Realtime
|
PostgreSQL Data Layer
|
+-- Row Level Security
Ground rules:
Browser -> talks only to Next.js
Next.js -> talks to Supabase
Supabase -> controls access via RLS
When the browser talks to multiple backend services directly, uncontrollable security boundaries emerge.
Part A - Architecture Decisions
These decisions are rarely changed and form the foundation.
A1 - Run Next.js as Its Own Service
Implementation
Next.js runs as its own container.
services
nextjs-app
supabase-stack
postgres
Example docker-compose:
services:
nextjs-app:
build: ./app
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_SUPABASE_URL=http://kong:8000
- NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}
networks:
- internal
Next.js must not run inside the Supabase stack.
Verifiable Condition
docker ps --format '{{.Names}}'
Expected:
nextjs-app
supabase-kong
supabase-postgres
supabase-auth
Failure Scenario
If Next.js runs in the same container as Supabase:
- Process space is shared
- Secrets live in the same environment
- A compromised Next.js server has direct access to all backend services
A2 - Browser Talks Only to Next.js
Implementation
The browser must only see a single public URL.
https://app.example.com
Not allowed:
https://app.example.com:8000
https://app.example.com:5432
https://app.example.com:9000
Firewall example:
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 8000 -j DROP
iptables -A INPUT -p tcp --dport 5432 -j DROP
iptables -A INPUT -p tcp --dport 9000 -j DROP
Verifiable Condition
nmap -p 443,3000,5432,8000,9000 app.example.com
Expected:
443 open
all others filtered
Failure Scenario
If Supabase Studio is publicly accessible:
- Full database access
- Schema manipulation
- Access to storage buckets
A3 - Set Security Headers
Next.js acts as a gateway and must set HTTP security headers.
Implementation
next.config.js
const securityHeaders = [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=()" },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
{
key: "Content-Security-Policy",
value: "default-src 'self'; img-src 'self' data: blob:; frame-ancestors 'none'"
}
]
Verifiable Condition
curl -I https://app.example.com
Expected headers:
X-Frame-Options
Content-Security-Policy
Strict-Transport-Security
Failure Scenario
Without CSP:
- XSS attacks can load external scripts
- Tokens can be exfiltrated
Part B - Implementation Checks
These rules apply to every code change.
B1 - Separate Environment Variables
An analysis of over 500 Next.js deployments shows that 23% of production Next.js instances expose at least one server-side environment variable in the client bundle (Snyk State of Open Source Security 2024).
Environment Variables Overview
| Variable | Visibility | Allowed Location | Risk if Leaked |
|---|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Client | .env, code | Low (public) |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Client | .env, code | Medium (RLS protects) |
| SUPABASE_SERVICE_ROLE_KEY | Server-only | .env, lib/supabase/admin.ts | Critical (bypasses RLS) |
| DATABASE_URL | Server-only | .env | Critical (full DB access) |
| TRIGGER_API_KEY | Server-only | .env | High (job execution) |
Client-visible:
NEXT_PUBLIC_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY
Server-only:
SUPABASE_SERVICE_ROLE_KEY
DATABASE_URL
TRIGGER_API_KEY
Verifiable Condition
grep -r "NEXT_PUBLIC_" .env*
Secrets must not appear there.
Build Leak Check
grep -r "SERVICE_ROLE" .next/
Expected:
no matches
Failure Scenario
service_role in the client bundle means:
- Full database access
- RLS completely ineffective
Anyone familiar with the fundamentals of secret management understands why this separation is essential.
B2 - Configure the Supabase SSR Client Correctly
Server client:
import { createServerClient } from "@supabase/ssr"
The server uses the anon key, not service_role.
Admin client:
createClient(url, SERVICE_ROLE_KEY)
For administrative tasks only.
Verifiable Condition
grep -rn "SERVICE_ROLE" app/
Expected: only in
lib/supabase/admin.ts
Failure Scenario
Server client with service_role:
- RLS is completely bypassed
- All requests have admin privileges
B3 - Middleware for Auth and Token Refresh
Middleware runs before every request.
middleware.ts
Implementation
const { data: { user } } = await supabase.auth.getUser()
Not:
getSession()
Verifiable Condition
grep getUser middleware.ts
Failure Scenario
Without middleware:
- Token refresh does not work
- Session breaks after 1h
B4 - Mutation Pattern
All mutations follow the same flow.
Auth
Input Validation
Ownership Check
Mutation
Logging
Example
const { user } = await supabase.auth.getUser()
if (!user) return
const post = await supabase
.from("posts")
.select("user_id")
.eq("id", postId)
if (post.user_id !== user.id)
throw new Error("forbidden")
Failure Scenario
Without ownership check:
deletePost(id)
A user can delete other users’ data.
B5 - Rate Limiting
Next.js has no built-in rate limiting.
Recommended solution:
Upstash Redis
Example
10 requests / minute / IP
Verifiable Condition
for i in {1..20}
do curl -X POST /api/login
done
Expected:
HTTP 429
B6 - Logging Without Secrets
Logs must not contain:
JWT Tokens
service_role keys
emails
passwords
Verifiable Condition
grep console.log app/
Part C - Operations
C1 - Dependency Updates
Next.js releases are frequent.
Check weekly:
npm audit
npm outdated
Recommended:
Renovate / Dependabot
C2 - Claude Code Integration
Architecture:
Git Push
|
Deterministic Checks
|
Security Report
|
Claude Analysis
|
DevOps Decision
Deterministic Checks
grep service_role
grep NEXT_PUBLIC
npm audit
Claude Analysis
Claude checks:
- New server actions
- New API routes
- Ownership patterns
- Input validation
- Architecture drift
Claude does not execute changes on production. The full audit workflow is described in Claude Code as a Security Control in DevOps Workflows.
Deployment Checklist
Check before every deployment:
[ ] Next.js runs as its own service
[ ] Supabase ports closed externally
[ ] Security headers active
[ ] No service_role in client bundle
[ ] service_role only in admin client
[ ] middleware.ts present
[ ] Server actions check auth
[ ] Ownership checks implemented
[ ] Login rate limit active
[ ] npm audit without critical findings
Conclusion
In the modern stack, Next.js is not a frontend - it is a privileged server layer.
Security comes from three levels:
Architecture
Implementation checks
Ongoing audits
The combination of CI security checks and Claude Code analysis catches both known patterns and emerging risks.
Anyone pursuing these principles together with a Cert-Ready-by-Design architecture builds verifiable security instead of retroactive audits.
Audit Checklist Download
Prepared prompt for Claude Code. Upload the file to your server and start Claude Code in your Next.js application's project directory. Claude Code will automatically check all security points from this runbook and report PASS, WARNING, or CRITICAL.
claude -p "$(cat claude-check-artikel-2-nextjs-en.md)" --allowedTools Read,Grep,Glob,Bash
Download checklistSeries Table of Contents
- Supabase Self-Hosting Runbook
- Running Next.js on Supabase Securely - this article
- Deploying Supabase Edge Functions Securely
- Running Trigger.dev Background Jobs Securely
- Claude Code as Security Control in DevOps Workflows
- Security Baseline for the Entire Stack
The next article describes how to deploy Supabase Edge Functions securely - without building a second backend architecture.

Bert Gogolin
CEO & Founder, Gosign
AI Governance Briefing
Enterprise AI, regulation, and infrastructure - once a month, directly from me.