Supabase with Next.js: The Complete Developer's Guide
Build full-stack apps with Supabase and Next.js using current SSR auth, Postgres, storage, realtime, Edge Functions, MCP, and RLS patterns.
Key Takeaways
Relational core
Current SSR auth helper
AI assistant integration
Production access control
In the rapidly evolving landscape of web development, the combination of Supabase and Next.js has emerged as a powerhouse solution for building modern, full-stack applications. This comprehensive guide will take you through every aspect of building production-ready applications with these technologies, from database design to realtime features, authentication flows to edge computing.
Ready to Build with Supabase?
Let our team help you leverage the full power of Supabase and Next.js for your next project. From architecture design to implementation, we've got you covered.
Start Your Supabase ProjectWhy Supabase + Next.js? The Perfect Full-Stack Combination
The combination of Supabase and Next.js represents a paradigm shift in how we build modern web applications. This pairing offers developers the best of both worlds: a powerful, open-source backend-as-a-service with a cutting-edge React framework. If you're still weighing your backend options, see how Supabase stacks up against Firebase before committing.
Supabase Advantages
- • PostgreSQL data that can be exported and migrated
- • Built-in email and social authentication
- • Realtime subscriptions out of the box
- • Edge Functions for serverless compute
- • S3-compatible object storage
- • Generous free tier for startups
Next.js Benefits
- • Server-side rendering for SEO
- • API routes for backend logic
- • Automatic code splitting
- • Built-in performance optimizations
- • TypeScript support out of the box
- • Vercel deployment integration
Setting Up Your Development Environment
Quick Start Guide
1. Create Next.js App with TypeScript
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
2. Install Supabase Client
npm install @supabase/supabase-js @supabase/ssr
3. Environment Variables
# .env.local (placeholders, never real credentials)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
APP_ORIGIN=http://localhost:3000
# Keep any secret/service-role key server-only; none is needed here.
@supabase/auth-helpers-nextjs package has been deprecated. This guide uses the new @supabase/ssr package, which is the recommended approach for all new projects. The new package provides better TypeScript support, improved security, and more flexible cookie handling.Database: Building with PostgreSQL at Scale
Supabase provides a full PostgreSQL database for every project, complete with extensions, realtime functionality, and automatic API generation. Let's explore how to leverage these features in your Next.js application.
Database Architecture
-- Disposable project migration; products are intentionally public.
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
username text unique not null
);
create table public.products (
id uuid primary key default gen_random_uuid(),
name text not null,
price numeric(10,2) not null check (price >= 0),
created_at timestamptz not null default now()
);
alter table public.profiles enable row level security;
alter table public.products enable row level security;
grant select on public.products to anon, authenticated;
grant select, insert, update on public.profiles to authenticated;
create policy "public catalog" on public.products
for select to anon, authenticated using (true);
Key Database Features
Automatic APIs
Instant REST APIs and optional GraphQL support generated from your schema
Database Functions
Write complex business logic directly in PostgreSQL
Triggers & Webhooks
React to database changes with automatic triggers
Extensions
PostGIS, pgvector for AI embeddings, pg_jsonschema, and other supported extensions (availability varies)
Implementing CRUD Operations in Next.js
Server Component Data Fetching
Create the helper below in lib/supabase/server.ts, then import it from the products page using your project alias. The fixture uses relative imports so the examples can be type-checked together. Its cookie-write catch is only for a Server Component with session refresh already handled by Proxy; a Route Handler must write cookies to its response.
// lib/supabase/server.ts (Server Components only; refresh in Proxy)
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!(url && key)) {
throw new Error("Missing public Supabase configuration");
}
const store = await cookies();
return createServerClient(url, key, {
cookies: {
getAll: () => store.getAll(),
setAll(values) {
try {
for (const { name, value, options } of values) {
store.set(name, value, options);
}
} catch {
// Server Components cannot write cookies; Proxy handles refresh.
}
},
},
});
}
// app/products/page.tsx; adapt the relative import to your file layout.
import { createClient } from "./server";
export default async function ProductsPage() {
const supabase = await createClient();
const { data, error } = await supabase
.from("products")
.select("id,name,price")
.order("created_at", { ascending: false })
.limit(20);
if (error) {
throw new Error("Could not load products");
}
return (
<ul>
{data?.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
Row Level Security (RLS)
Row Level Security is Supabase's killer feature for building secure applications. It allows you to define access rules at the database level.
-- Apply after schema.sql; policy and table grants are both required.
create policy "read own profile" on public.profiles
for select to authenticated using ((select auth.uid()) = id);
create policy "create own profile" on public.profiles
for insert to authenticated with check ((select auth.uid()) = id);
create policy "update own profile" on public.profiles
for update to authenticated using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
Authentication: Complete Security Implementation
Supabase provides a comprehensive authentication solution that integrates seamlessly with Next.js. From social logins to magic links, multi-factor authentication to session management, provider configuration, plan limits and permission tests still determine what is ready for your application.
Authentication Methods Supported
Social Providers
- • GitHub
- • Discord
- • Twitter/X
- • Other configured OAuth providers
Email Methods
- • Email/Password
- • Magic Links
- • OTP Codes
- • Email Change
- • Password Reset
Advanced Features
- • Phone Auth
- • MFA/2FA
- • SAML SSO
- • Custom JWT
- • Anonymous Users
Implementing Authentication in Next.js
1. Setting Up Auth Helpers
// lib/supabase/browser.ts
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!(url && key)) {
throw new Error("Missing public Supabase configuration");
}
return createBrowserClient(url, key);
}
2. Social Login Implementation
"use client";
import { useState } from "react";
import { createClient } from "./browser";
export function SocialLogin() {
const [message, setMessage] = useState("");
async function login() {
const supabase = createClient();
const { error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: `${location.origin}/auth/callback` },
});
if (error) {
setMessage("Sign-in failed. Please try again.");
}
}
return (
<div>
<button onClick={login} type="button">
Sign in with Google
</button>
<p role="status">{message}</p>
</div>
);
}
3. Protected Routes with Proxy (Next.js 16)
// proxy.ts on Next.js 16; middleware.ts + middleware on Next.js 15.
import { createServerClient } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export async function proxy(request: NextRequest) {
const appOrigin = process.env.APP_ORIGIN;
if (!appOrigin) {
throw new Error("Missing trusted APP_ORIGIN");
}
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!(url && key)) {
throw new Error("Missing public Supabase configuration");
}
let response = NextResponse.next({ request });
response.headers.set("Cache-Control", "private, no-store");
const supabase = createServerClient(url, key, {
cookies: {
getAll: () => request.cookies.getAll(),
setAll(values, headers) {
for (const { name, value } of values) {
request.cookies.set(name, value);
}
const previous = response;
response = NextResponse.next({ request });
for (const cookie of previous.cookies.getAll()) {
response.cookies.set(cookie);
}
for (const key of ["cache-control", "expires", "pragma"]) {
const value = previous.headers.get(key);
if (value) {
response.headers.set(key, value);
}
}
for (const { name, value, options } of values) {
response.cookies.set(name, value, options);
}
for (const [key, value] of Object.entries(headers)) {
response.headers.set(key, value);
}
},
},
});
const { data, error } = await supabase.auth.getClaims();
if (
request.nextUrl.pathname.startsWith("/dashboard") &&
(error || !data?.claims?.sub)
) {
const redirect = NextResponse.redirect(new URL("/login", appOrigin));
for (const cookie of response.cookies.getAll()) {
redirect.cookies.set(cookie);
}
for (const key of ["cache-control", "expires", "pragma"]) {
const value = response.headers.get(key);
if (value) {
redirect.headers.set(key, value);
}
}
return redirect;
}
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*", "/auth/:path*"],
};
Storage: File Management Made Simple
Supabase Storage provides an S3-compatible object storage solution that integrates seamlessly with your database. Store, organize, and serve files with automatic image optimization and CDN delivery.
Storage Architecture
Key Features
- Image transformations (Pro and above)
- CDN distribution with caching
- Direct browser uploads
- RLS policies for file access
Use Cases
- User profile avatars
- Product images and galleries
- Document storage
- Video file delivery
Implementing File Uploads
Next.js Upload Component
Create a private avatars bucket and an INSERT policy on storage.objects restricted to bucket_id = 'avatars' and the first path folder equal to auth.uid(). Configure MIME and file-size limits in the bucket. This component resets its busy state on success and failure and does not pretend a public URL makes a private object accessible. See Storage access control.
"use client";
import { type ChangeEvent, useState } from "react";
import { createClient } from "./browser";
export function ImageUpload() {
const [uploading, setUploading] = useState(false);
const [message, setMessage] = useState("");
async function upload(file: File) {
setUploading(true);
try {
const supabase = createClient();
const {
data: { user },
error: authError,
} = await supabase.auth.getUser();
if (authError || !user) {
throw new Error("Sign in first");
}
const path = `${user.id}/${crypto.randomUUID()}`;
const { error } = await supabase.storage
.from("avatars")
.upload(path, file, { upsert: false });
if (error) {
throw error;
}
setMessage("Upload saved in your private bucket.");
} catch {
setMessage("Upload failed. Please try again.");
} finally {
setUploading(false);
}
}
async function onChange(event: ChangeEvent<HTMLInputElement>) {
const file = event.currentTarget.files?.[0];
if (file) {
await upload(file);
}
}
return (
<div>
<input
aria-label="Avatar"
disabled={uploading}
type="file"
onChange={onChange}
/>
<p role="status">{message}</p>
</div>
);
}
Image Transformations
Supabase Storage provides on-the-fly image transformations on Pro plans and above. The example requests a 200 × 200 thumbnail at quality 80; these are chosen inputs, not measured speed or quality outcomes. WebP negotiation is automatic, so the earlier unsupported format: "webp" option is removed. See the image transformation reference.
import { createClient } from "./browser";
export function thumbnailUrl() {
const supabase = createClient();
const { data } = supabase.storage.from("products").getPublicUrl("image.jpg", {
transform: { width: 200, height: 200, resize: "cover", quality: 80 },
});
return data.publicUrl;
}
Realtime: Building Live Features
Supabase Realtime enables you to build interactive features like live chat, collaborative editing, and real-time dashboards. It uses PostgreSQL's built-in replication functionality to stream database changes to your application.
Realtime Capabilities
Database Changes
Listen to changes on published tables with suitable permissions
Presence
Track online users and their state in real-time
Broadcast
Send messages between clients without database persistence
Building a Live Chat Feature
1. Database Schema
This miniature schema allows each user to read and insert their own messages. A room filter controls delivery, not permission. A collaborative room needs a separate membership policy and tests; publication alone does not provide access control. See Postgres Changes setup and RLS.
-- Demonstrates a user's own messages, not a shared-room permission model.
create table public.messages (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
room_id uuid not null,
content text not null
);
alter table public.messages enable row level security;
grant select, insert on public.messages to authenticated;
create policy "read own messages" on public.messages
for select to authenticated using ((select auth.uid()) = user_id);
create policy "insert own messages" on public.messages
for insert to authenticated with check ((select auth.uid()) = user_id);
alter publication supabase_realtime add table public.messages;
2. Realtime Subscription Utility
import { createClient } from "./browser";
export function subscribeToMessages(
roomId: string,
onInsert: (row: Record<string, unknown>) => void
) {
const supabase = createClient();
const channel = supabase
.channel(`room:${roomId}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.${roomId}`,
},
(payload) => onInsert(payload.new)
)
.subscribe();
return () => {
supabase.removeChannel(channel).catch(() => {
console.error("Channel cleanup failed");
});
};
}
Presence: Who's Online
Presence metadata is supplied by clients, so a displayed user_id is not proof of identity. Use authorized private channels where access is sensitive, and remove the channel when the view ends. The example demonstrates lifecycle handling, not an authenticated roster.
import { createClient } from "./browser";
export function trackPresence(userId: string) {
const supabase = createClient();
const channel = supabase.channel("online-users");
channel
.on("presence", { event: "sync" }, () => {
console.log(channel.presenceState());
})
.subscribe(async (status) => {
if (status === "SUBSCRIBED") {
await channel.track({ user_id: userId });
}
});
return () => {
supabase.removeChannel(channel).catch(() => {
console.error("Channel cleanup failed");
});
};
}
Edge Functions: Serverless at the Edge
Supabase Edge Functions are globally distributed TypeScript functions with a Deno runtime. Database location, network requests and cold starts affect latency; choose regions against the actual workload. They support webhooks, authentication, third-party integrations, and complex business logic.
Edge Functions vs Next.js API Routes
Edge Functions
- • Globally distributed
- • Deno runtime
- • Direct database access
- • Triggered by webhooks/cron
- • Independent deployment
API Routes
- • Part of Next.js app
- • Node.js runtime
- • Frontend integration
- • Shared deployment
- • Vercel optimizations
Creating an Edge Function
Example: Webhook Verification
Correction, October 4: the earlier handler parsed unsigned JSON and could mark an order paid. This replacement verifies the raw body and fails closed, but intentionally performs no payment processing. Its successful response is only a development demonstration. Production needs durable event storage, idempotency, retries and reconciliation before acknowledgement; use our webhook reliability reference and the official Stripe Edge Function example.
// supabase/functions/stripe-webhook/index.ts; verification-only example.
import Stripe from "npm:stripe@22.6.2";
declare const Deno: {
env: { get(name: string): string | undefined };
serve(handler: (request: Request) => Promise<Response>): unknown;
};
const apiKey = Deno.env.get("STRIPE_API_KEY");
const secret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
if (!(apiKey && secret)) {
throw new Error("Missing server-only Stripe secrets");
}
const stripe = new Stripe(apiKey);
const cryptoProvider = Stripe.createSubtleCryptoProvider();
Deno.serve(async (request: Request) => {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const signature = request.headers.get("stripe-signature");
if (!signature) {
return new Response("Missing signature", { status: 400 });
}
const body = await request.text();
try {
await stripe.webhooks.constructEventAsync(
body,
signature,
secret,
undefined,
cryptoProvider
);
} catch {
return new Response("Invalid signature", { status: 400 });
}
// Deliberately no database writes. Add durable idempotent processing before
// acknowledging payment events in a production handler.
return new Response("Verified sample; no payment processed", { status: 200 });
});
Running the verification sample locally
# Disposable development project only; review the project ref first.
supabase functions serve stripe-webhook --no-verify-jwt
# Stripe sends its signature, not a Supabase JWT. Keep signature verification.
# Production deployment requires an idempotent handler, not this no-op sample.
MCP Servers: AI-Powered Development with Supabase
Model Context Protocol (MCP) servers can give AI assistants controlled access to Supabase project context, making it easier to inspect schemas, draft SQL, manage migrations, and reason about Row Level Security policies during development.
What are MCP Servers?
MCP (Model Context Protocol) servers are standardized interfaces that allow AI assistants to interact with external tools and services. For Supabase developers, this means your AI coding assistant can:
- Query and modify your database schema directly
- Generate type-safe database queries and mutations
- Create and test RLS policies with real-time feedback
- Deploy edge functions and monitor their performance
Supabase MCP Server Setup
1. Hosted development connection
{
"mcpServers": {
"supabase": {
"type": "http",
"url": "https://mcp.supabase.com/mcp?project_ref=YOUR_DEV_PROJECT_REF&read_only=true"
}
}
}
Correction, October 4: the earlier @supabase/mcp-server install and supabase-mcp init commands were not the documented setup. Use the official hosted MCP endpoint, a scoped development project, read-only access and reviewed tool calls. Read-only mode can still disclose data and does not neutralize prompt injection.
The capability list above describes potential tools, not the permissions of this connection. The supplied read-only configuration cannot perform schema writes, migrations or deployments. VS Code's MCP setup documentation covers its supported client configuration.
2. IDE Integration
Cursor AI
Use the MCP setup supplied by your client and verify the project scope before authorizing the connection
VS Code
VS Code supports configured MCP servers; Continue is not required by the official VS Code setup
Windsurf AI
Check your installed client's supported transport and authentication flow before connecting
Real-World MCP Workflows
Example: AI-Assisted Database Design
Ask the assistant to propose a migration and its RLS tests.
Review the SQL, grants, tenant model and generated types in version control.
Apply only to a disposable development project after explicit approval.
Read-only MCP cannot apply migrations; do not treat generated SQL as tested.
Production Best Practices
Review these security, performance and scaling practices against your application's requirements before production use.
Security Best Practices
- Always enable RLS on sensitive tables
- Use service role key only on server-side
- Implement proper CORS policies
- Configure and review audit logs with pgaudit
- Environment-specific API keys
Performance Optimization
- Index frequently queried columns
- Choose a suitable Supavisor connection-pool mode
- Implement query result caching
- Optimize realtime subscriptions
- Use CDN for static assets
Use the connection guide to choose direct, session or transaction pooling. Transaction pooling has prepared-statement restrictions; check the mode against your driver. Enable pg_stat_statements before the query below. Its 100 ms threshold and 20-row limit are example settings, not benchmark results.
Monitoring & Observability
Essential Monitoring Setup
1. Database Monitoring
-- Example alert threshold, not a measured latency target.
select query, calls, mean_exec_time
from pg_stat_statements
where mean_exec_time > 100
order by mean_exec_time desc
limit 20;
2. API Performance Tracking
// Server-side timing wrapper; send only a fixed route label to your logger.
export async function measureProducts<T>(
operation: () => Promise<T>,
log: (entry: { route: string; durationMs: number; ok: boolean }) => void
) {
const start = performance.now();
let ok = false;
try {
const result = await operation();
ok = true;
return result;
} finally {
log({ route: "/api/products", durationMs: performance.now() - start, ok });
}
}
Cost Optimization
Cost Management Strategies
Database Optimization
- • Use appropriate column types
- • Archive old data to cold storage
- • Implement data retention policies
- • Monitor database size growth
Resource Management
- • Optimize image sizes before upload
- • Set appropriate cache headers
- • Use edge functions judiciously
- • Monitor bandwidth usage
Building a Complete Application: Task Management System
Use the following owner-only task workspace as a starting point. The feature list is a roadmap: these excerpts do not implement a complete collaborative application, attachments, notifications or optimistic updates.
Application Features
Planned Functionality
- ✓ User authentication with social logins
- ✓ Real-time task updates across devices
- ✓ File attachments with image previews
- ✓ Team collaboration with presence
- ✓ Email notifications via edge functions
Technical Checklist
- ✓ Server-side rendering for SEO
- ✓ Optimistic UI updates
- ✓ Row Level Security for data isolation
- ✓ Responsive design with Tailwind
- ✓ TypeScript for type safety
Step-by-Step Implementation
Database Schema Design
This example is owner-only. Team membership, invitations, roles, billing and collaboration remain application work. Apply migrations to a disposable project and test cross-owner denial before extending permissions.
-- Owner-only workspace example, not team membership or role delegation.
create table public.workspaces (
id uuid primary key default gen_random_uuid(),
name text not null,
owner_id uuid not null references auth.users(id)
);
create table public.tasks (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces(id),
title text not null
);
alter table public.workspaces enable row level security;
alter table public.tasks enable row level security;
grant select, insert, update, delete on public.workspaces, public.tasks to authenticated;
create policy "owner workspace" on public.workspaces to authenticated
using ((select auth.uid()) = owner_id)
with check ((select auth.uid()) = owner_id);
create policy "owner tasks" on public.tasks to authenticated
using (exists (select 1 from public.workspaces w where w.id = workspace_id and w.owner_id = (select auth.uid())))
with check (exists (select 1 from public.workspaces w where w.id = workspace_id and w.owner_id = (select auth.uid())));
alter publication supabase_realtime add table public.tasks;
Authentication Flow
Enable the Google provider and register the exact application callback URL in the redirect allowlist. APP_ORIGIN is a trusted deployment setting, not a value taken from the request host. The callback exchanges a PKCE code once, writes cookies to the returned response, and sends missing or rejected codes to a fixed login error route. Email OTP confirmation uses a different flow. See Google OAuth configuration and exchangeCodeForSession.
// app/auth/callback/route.ts (OAuth PKCE callback)
import { createServerClient } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const appOrigin = process.env.APP_ORIGIN;
if (!appOrigin) {
throw new Error("Missing trusted APP_ORIGIN");
}
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
if (!(url && key)) {
throw new Error("Missing public Supabase configuration");
}
const origin = new URL(appOrigin);
const fail = () => {
const response = NextResponse.redirect(
new URL("/login?error=auth", origin)
);
response.headers.set("Cache-Control", "private, no-store");
return response;
};
const code = new URL(request.url).searchParams.get("code");
if (!code) {
return fail();
}
const response = NextResponse.redirect(new URL("/dashboard", origin));
response.headers.set("Cache-Control", "private, no-store");
const supabase = createServerClient(url, key, {
cookies: {
getAll: () => request.cookies.getAll(),
setAll(values, headers) {
for (const { name, value, options } of values) {
request.cookies.set(name, value);
response.cookies.set(name, value, options);
}
for (const [key, value] of Object.entries(headers)) {
response.headers.set(key, value);
}
},
},
});
const { error } = await supabase.auth.exchangeCodeForSession(code);
return error ? fail() : response;
}
Real-time Task Updates
Subscribe only after loading the initial authorized task list. The callback reloads that list after a change; it is not a durable event log. Clean up on component unmount or workspace change and test reconnect reconciliation. RLS does not authorize DELETE notifications. The documentation allows DELETE filtering only with REPLICA IDENTITY FULL, which this schema does not set. Authorized reloads protect the displayed rows, not event-feed privacy; this sample does not provide tenant-private deletion notifications.
import { createClient } from "./browser";
export function subscribeToTasks(workspaceId: string, reload: () => void) {
const supabase = createClient();
const channel = supabase
.channel(`workspace:${workspaceId}`)
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "tasks",
filter: `workspace_id=eq.${workspaceId}`,
},
() => reload()
)
.subscribe();
return () => {
supabase.removeChannel(channel).catch(() => {
console.error("Channel cleanup failed");
});
};
}
Conclusion & Next Steps
The combination of Supabase and Next.js represents a powerful paradigm for building modern web applications. With Supabase handling your backend infrastructure and Next.js providing a world-class frontend framework, you can focus on building features that matter to your users.
Key Takeaways
What We've Covered
- ✓ Complete database setup with PostgreSQL
- ✓ Authentication flows and security
- ✓ File storage and CDN integration
- ✓ Real-time features and presence
- ✓ Edge functions for serverless compute
- ✓ Production best practices
Next Steps
- → Implement advanced RLS policies
- → Explore database functions and triggers
- → Set up monitoring and alerting
- → Optimize for scale with caching
- → Integrate third-party services
- → Deploy to production
Resources for Continued Learning
Official Documentation
Community & Support
Trust boundaries
October 4, 2026: architecture you can verify
Use this flow to decide which client belongs at each boundary. A publishable key identifies the project; the user's verified token and database policies govern access. Put privileged keys in server-only jobs with separate authorization. They are not a shortcut for browser queries.
- Browser → OAuth provider → /auth/callback: exchange the PKCE code and return session cookies.
- Browser → Next.js Proxy → Server Component: verify claims, refresh cookies on the request and response, and keep auth responses private.
- Server or browser user client → Supabase Data API → Postgres grants + RLS: allow the user's intended rows.
- Browser → Storage or Realtime: apply the feature's access policies; an object URL or subscription filter is not authorization.
The code above is checked against this repository's frozen fixture: Next.js 16.3.5, @supabase/ssr 0.12.7, supabase-js 2.116.0, React 19.3.0 and Node 24.21.0. These are tested inputs, not a claim that every version is the newest. The isolated TypeScript files compile against those installed APIs. JavaScript behavior was exercised with mocked cookies, identity responses, storage and channels; the webhook's signature verifier was mocked too. SQL migrations and CLI commands were reviewed but were not executed against a Supabase project. No live login, database mutation or production deployment is claimed.
Next.js documents awaited cookies() and the Proxy file convention. On Next.js 15 use middleware.ts and export middleware instead. Supabase's SSR guide distinguishes getClaims() verification, getUser() for a current user record and getSession() for raw session data. Do not authorize from an unverified getSession() user object.
Proxy refresh is not complete authorization. This sample redirects the dashboard, while each protected Route Handler and Server Action must still verify identity and permissions before acting. Including /api in a refresh matcher does not make its endpoints private.
The Proxy keeps the response that receives refreshed cookies, including cache headers supplied by the installed SSR helper. It copies that state when redirecting. The Server Component helper cannot guarantee browser refresh by itself. Apply Proxy only to routes that use auth, and keep personalized reads and Set-Cookie responses out of shared CDN caches. Use the key-type reference when migrating older anon/service-role configurations.
| Boundary | Try | Required result |
|---|---|---|
| Callback | Valid, absent and rejected code; changed request host | Only valid code reaches the configured dashboard; failures reach the fixed login route. |
| Refresh | Expired token, anonymous request and repeated cookie writes | Verified identity or denial, refreshed cookies retained, private cache headers preserved. |
| Database | Anonymous and second-owner select, insert, update and delete | Only deliberately public products are public; private owner rows cannot cross owners. |
| Storage | Wrong folder, wrong bucket, oversized file and upload failure | Policy or bucket limit denies access; UI recovers; private objects need authorized serving. |
| Realtime | Wrong workspace, reconnect, delete and unmount | Verify authorized reloads and channel cleanup. Inspect DELETE delivery separately: this sample does not guarantee a tenant-private deletion feed. |
Mocked tests can catch missing error branches and lost response cookies. They cannot demonstrate that a hosted policy denies another tenant, a provider redirect is configured correctly, or a CDN respects your cache headers. Run those checks with separate test identities on a disposable project, then inspect deployed response headers. Log outcomes without tokens or personal data. A passing local example is the start of that review, not its completion.
Budget assumptions
Price the complete application
Checked October 4, the Supabase pricing page still lists Free at 500 MB database size, 1 GB file storage and 50,000 monthly active users. Pro starts at USD $25/month and lists 8 GB disk per project, 100 GB file storage and 100,000 monthly active users. Paid plans include USD $10/month compute credits; additional projects and larger compute affect the bill. Free projects pause after one week of inactivity. Add egress, add-ons and the Next.js host to your estimate rather than treating a plan's base price as your total.
If you need MongoDB, this guide does not describe a native replacement for Supabase's Postgres database. Keep MongoDB credentials in a separate server integration, decide which store owns each entity, and design retries or reconciliation for writes across both. For ORM and connection-pool decisions, read the Prisma production guide. For compiler upgrades, use the supabase-js TypeScript migration checklist.
Let's Build Something Amazing Together
Our team specializes in building scalable applications with Supabase and Next.js. From architecture design to implementation, we can help review permissions, callback behavior and deployment assumptions before launch.
Frequently Asked Questions
Deep dives on AI, marketing and development.
Practical guides and fresh insights by email. No recycled takes.
Related Guides
Explore more guides on building modern full-stack applications with cutting-edge tools