Skip to content
Infrastructure & Technology

Running Next.js on Supabase Securely

DevOps runbook for Next.js on Supabase: architecture, middleware, auth patterns, rate limiting, and Claude Code integration.

Mansoor Ahmed
Mansoor Ahmed
Head of Engineering 14 min read

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.

  1. Supabase Self-Hosting Runbook
  2. Running Next.js on Supabase Securely - this article
  3. Deploying Supabase Edge Functions Securely
  4. Running Trigger.dev Background Jobs Securely
  5. Claude Code as Security Control in DevOps Workflows
  6. 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

VariableVisibilityAllowed LocationRisk if Leaked
NEXT_PUBLIC_SUPABASE_URLClient.env, codeLow (public)
NEXT_PUBLIC_SUPABASE_ANON_KEYClient.env, codeMedium (RLS protects)
SUPABASE_SERVICE_ROLE_KEYServer-only.env, lib/supabase/admin.tsCritical (bypasses RLS)
DATABASE_URLServer-only.envCritical (full DB access)
TRIGGER_API_KEYServer-only.envHigh (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 checklist

Series Table of Contents

  1. Supabase Self-Hosting Runbook
  2. Running Next.js on Supabase Securely - this article
  3. Deploying Supabase Edge Functions Securely
  4. Running Trigger.dev Background Jobs Securely
  5. Claude Code as Security Control in DevOps Workflows
  6. 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

Bert Gogolin

CEO & Founder, Gosign

AI Governance Briefing

Enterprise AI, regulation, and infrastructure - once a month, directly from me.

No spam. Unsubscribe anytime. Privacy policy

Next.js Supabase Security DevOps Self-Hosting
Share this article

Frequently Asked Questions

Why must Next.js not run in the same container as Supabase?

When Next.js runs in the same container as Supabase, the process space is shared and secrets live in the same environment. A compromised Next.js server then has direct access to all backend services.

Why must the service_role key never be used in client code?

The service_role key bypasses all Row Level Security policies. If it ends up in the client bundle, every user has full database access and RLS becomes completely ineffective.

What happens if a Server Action has no auth check?

Without a getUser() check in a Server Action, any user with a valid session cookie can call the action, even for other users' data. Since Server Actions are reachable via HTTP POST, a simple curl command with a stolen cookie is sufficient.