JSTAcademy
0 XP
Dashboard
Crash Courses
Supabase in Production
13 min
Masters+150 XP
Crash Courses · Masters

Supabase in Production

Row Level Security, migrations, connection pooling, and the env setup that does not leak secrets.
13 min read+150 XP on completionCert: Ship It Crash Course
Tap any word in the text below to start reading from there.

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 ONLY

Anon 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/postgres

In 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

  1. Vercel Dashboard → Your Project → Settings → Environment Variables
  2. Add each variable with the correct scope (Production / Preview / Development)
  3. 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
0%