Supabase in Production
Supabase in Production
The Two Keys You Must Understand
Every Supabase project has two main keys. Mixing them up is one of the most common security mistakes:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://abcdefgh.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # SAFE for browser
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # SERVER ONLYAnon key: Used in client-side Supabase client. Respects RLS policies. Safe to expose.
Service role key: Used in API routes and server actions only. Bypasses ALL RLS policies. Full database access. If this leaks into your client bundle, anyone can read/write/delete everything.
---
Row Level Security — Why and How
Without RLS, anyone with your anon key can read every row in every table. Supabase ships with RLS disabled by default. You must enable it.
-- Enable RLS on a table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Policy: users can only read their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
-- Policy: users can only update their own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = user_id);
-- Policy: public read for a posts table
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
USING (published = true);After enabling RLS with no policies: zero rows are returned for any query. Policies are additive — add one for each operation (SELECT, INSERT, UPDATE, DELETE).
---
Migrations: The Right Way to Change Your Schema
Never change production schema in the Supabase dashboard GUI directly. Use migrations.
# Install Supabase CLI
npm install -g supabase
# Link to your project
supabase login
supabase link --project-ref your-project-ref
# Create a new migration
supabase migration new add_courses_table
# Creates: supabase/migrations/20241003120000_add_courses_table.sql
# Write your SQL-- supabase/migrations/20241003120000_add_courses_table.sql
CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE courses ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users manage own courses"
ON courses FOR ALL
USING (auth.uid() = user_id);# Apply to production
supabase db push---
Connection Pooling for Serverless
Vercel serverless functions spin up and down constantly. Each function invocation opens a new database connection. At scale this exhausts Postgres's connection limit (default 100).
Supabase's Transaction Pooler solves this:
# Direct connection (use for migrations only)
postgresql://postgres:password@db.xxx.supabase.co:5432/postgres
# Pooled connection (use in your app)
postgresql://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:6543/postgresIn your Supabase client:
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { getAll: () => cookieStore.getAll() } }
)
}---
Vercel Environment Variable Setup
- Vercel Dashboard → Your Project → Settings → Environment Variables
- Add each variable with the correct scope (Production / Preview / Development)
- After adding variables, trigger a new deployment (changes don't apply to already-built deployments)
Never commit `.env.local` to git. Verify your .gitignore:
.env.local
.env.*.local