How to Add Supabase to Next.js (2026 Step-by-Step Guide)
Supabase gives you a Postgres database, auth, file storage, and realtime subscriptions behind one SDK. This guide takes you from zero to a working Next.js + Supabase app: project setup, type-safe client, Row Level Security for auth, server-side fetching, and the gotchas with the Next.js App Router.
What Supabase actually is
Supabase is a hosted Postgres with a REST + realtime layer on top, plus prebuilt auth and file storage. You write SQL (or use their query builder) for everything; auth is JWT-based and integrates with Postgres Row Level Security (RLS) so authorization is enforced at the database, not in your application code. The whole package competes with Firebase, but on top of real SQL.
When to pick Supabase: you want Postgres, you want auth without rolling your own, and you want it to scale to real traffic without managing infrastructure. When NOT to pick: you specifically need a particular ORM (Drizzle, Prisma) — they work but aren't the canonical path; or you want zero vendor lock-in (the auth tables are Supabase-specific).
Step 1 — Create the project + install the SDK
supabase.com → New project → pick a region close to your users. Two URLs and two keys: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY` (safe to expose; respects RLS), and `SUPABASE_SERVICE_ROLE_KEY` (NEVER expose — bypasses RLS, used only in server code).
`npm i @supabase/supabase-js @supabase/ssr`. The `@supabase/ssr` package handles the Next.js App Router cookie pattern — it's what reads the auth session in Server Components.
Create `lib/supabase/client.ts` (browser client using anon key), `lib/supabase/server.ts` (server client using cookies from headers, also anon key but with the session attached), and `lib/supabase/admin.ts` (service-role client for admin operations — never imported in Client Components).
Step 2 — Design your schema with RLS
Open the SQL editor in Supabase. Create your tables. The killer feature is Row Level Security — policies that filter which rows a user can read/write based on their `auth.uid()`. With RLS on, you literally can't accidentally leak another user's data, because the database itself enforces the check.
Example: `CREATE TABLE projects (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid REFERENCES auth.users, name text NOT NULL); ALTER TABLE projects ENABLE ROW LEVEL SECURITY; CREATE POLICY "users see own projects" ON projects FOR SELECT USING (auth.uid() = user_id);` — now every query against `projects` is automatically filtered to rows the calling user owns.
Generate TypeScript types: `npx supabase gen types typescript --project-id=YOUR_REF > lib/database.types.ts`. Pass this to `createClient<Database>()` and you get full type-safety on every query.
Step 3 — Wire authentication
Supabase auth supports email/password, magic links, OAuth (Google, GitHub, Apple, etc.), and one-time passwords. Configure providers in the dashboard → Authentication → Providers.
On the client: `await supabase.auth.signInWithPassword({ email, password })` returns a session. The session is stored in cookies (using `@supabase/ssr`) so it survives across pages and works server-side.
Protect pages in a Server Component: `const { data: { user } } = await supabase.auth.getUser(); if (!user) redirect('/login')`. The auth check happens at the database boundary thanks to RLS — even if you forget the user check, the data won't leak.
Step 4 — Fetch data server-side
In a Server Component (the default in App Router): `const supabase = createServerClient(...)`. Then `const { data: projects } = await supabase.from('projects').select('*').order('created_at', { ascending: false })`. The query runs server-side, types come from your generated types file, RLS filters automatically.
For mutations, use Server Actions: a function marked `'use server'` that calls `supabase.from('projects').insert({ name }).select().single()`. Server Actions are tied to form submissions and revalidate cached data automatically.
Avoid mixing client-side and server-side fetching for the same data — pick one path per page. Server Components fetch on the server (faster initial paint, no client bundle). Client Components fetch with `useQuery` (interactive features, optimistic updates).
Step 5 — File storage (if you need it)
Create a bucket in the dashboard → Storage. Set its RLS policies (e.g., 'users can read their own files'). Upload from the client: `await supabase.storage.from('avatars').upload(\`\${userId}/avatar.png\`, file)`. Files get a public URL or a signed temporary URL depending on the bucket's policy.
Avoid storing huge files (>50MB) — Supabase storage isn't optimized for it, and S3 directly is cheaper at scale. Use it for user avatars, document attachments, small media — the long tail of small files apps need.
Step 6 — Realtime (optional)
`const channel = supabase.channel('public:projects').on('postgres_changes', { event: '*', schema: 'public', table: 'projects' }, (payload) => { /* update local state */ }).subscribe()` — your client gets a websocket message every time the `projects` table changes.
Use this for collaborative features: presence (who's online), live counters, multiplayer editors. Don't use it as a substitute for proper data fetching — it's an event stream, not a query API.
Production gotchas
Connection pooling: Supabase has two ports — 5432 (direct) and 6543 (pooler). Use 6543 in serverless environments (Vercel, AWS Lambda) — direct connections will exhaust the database limit fast under traffic.
Backups: Supabase backs up daily for free, hourly with Pro. For mission-critical data, add an external backup pipeline (pg_dump to S3 nightly via a cron job).
Costs: free tier is 500MB database + 1GB storage + 50k MAU. Pro is $25/mo. Once you exceed those, costs scale linearly. Monitor in the dashboard → Reports.
How to do it
- 1
Create the Supabase project
supabase.com → New project → pick a region. Copy the URL + anon key + service-role key into .env.local.
- 2
Install the SDK
npm i @supabase/supabase-js @supabase/ssr. Create lib/supabase/{client,server,admin}.ts for the three contexts (browser, server, admin).
- 3
Design the schema with RLS
Open the SQL editor → create tables → ENABLE ROW LEVEL SECURITY on each → write policies that filter by auth.uid().
- 4
Wire authentication
Configure providers in dashboard → Authentication. signInWithPassword / signInWithOAuth on the client. getUser() on the server to gate Server Components.
- 5
Generate TypeScript types
npx supabase gen types typescript --project-id=YOUR_REF > lib/database.types.ts. Pass this generic to createClient<Database>() for full type-safety.
- 6
Switch to the pooler URL for prod
Vercel + Supabase = use port 6543 (transaction-mode pooler), not 5432 (direct). Direct connections exhaust the limit under serverless traffic.
Frequently asked questions
Supabase vs Firebase?
Both are 'backend in a box' for app developers. Supabase = SQL + Postgres ecosystem. Firebase = NoSQL document store. Pick Supabase if you want SQL (joins, transactions, mature tooling) or want easier eject path (it's just Postgres). Pick Firebase if you want a fully managed NoSQL experience with Google's mobile-first SDKs.
Can I use Prisma or Drizzle with Supabase?
Yes — Supabase is just Postgres, so any ORM works. But you'll lose RLS as the authorization layer (your ORM runs as the service role, bypassing policies). The trade-off: type-safety + ORM ergonomics vs database-enforced security. For SaaS, the canonical Supabase path (their SDK + RLS) is more secure. For data-heavy apps, Drizzle on top of Supabase Postgres is fine.
Does Supabase work with the Next.js App Router?
Yes — use the @supabase/ssr package, which handles the App Router's async cookie API. It exposes createBrowserClient (for Client Components), createServerClient (for Server Components and Route Handlers), and supports Server Actions. The session is read from cookies on every request.
What if I outgrow Supabase?
Migration is straightforward because it's Postgres. Run pg_dump from Supabase, restore to RDS or self-hosted Postgres, swap your client to a direct pg connection. You'll lose Supabase Auth (you'd need to migrate to Auth.js or Clerk) and Supabase Storage (move to S3). The data layer is portable; the platform glue isn't.
Ready to build?
Try InBuild for free — describe what you want, get a complete site in 30 seconds, export the code anytime.
Start freeMore guides
The Ultimate Guide to AI Website Builders (2026)
How AI website builders work, what they're good at, where they fall short, and how to pick one — Lovable, v0, Bolt, Replit, InBuild compared with honest tradeoffs.
ReadTutorialThe Complete Guide to SaaS Landing Pages (2026)
Anatomy of a SaaS landing page that converts: hero, social proof, features, pricing, FAQ. What every section does, what good looks like, and how to build it in 30 seconds.
Read