Supabase is the fastest way to get a production Postgres database with auth, file storage, and realtime — without managing infrastructure. In 2026, the platform has matured considerably: the CLI handles migrations, the JS client is on v3, and type generation is first-class. This guide covers project setup through production-ready patterns.
What changed in 2026
- supabase-js v3 drops the legacy
GoTrueClient in favour of a unified SupabaseClient; session refresh is automatic.
- Supabase CLI v2 handles local dev with Docker Compose, migrations, seeding, and type generation in one tool.
- Edge Functions are GA — Deno-based serverless functions co-located with your project; useful for webhooks and server-side auth flows.
- pgvector is built-in — every Supabase project ships with the pgvector extension available; enable it with one SQL command.
Project and CLI setup
npm install -g supabase
supabase login
supabase init # creates supabase/ directory
supabase start # spins up local Postgres + Auth + Studio
supabase start pulls Docker images the first time (~1 min). After that it's fast. You get a local API URL and anon key printed to stdout.
Install the client library:
npm install @supabase/supabase-js
Initialize:
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
);
Creating a table with RLS
-- supabase/migrations/20260601_create_notes.sql
create table notes (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
body text not null,
created_at timestamptz default now()
);
alter table notes enable row level security;
create policy "Users can manage their own notes"
on notes
for all
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
Apply locally: supabase db push. Apply to remote: supabase db push --db-url <remote-connection-string>.
Querying with the typed client
Generate types:
supabase gen types typescript --local > src/database.types.ts
Use them:
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabase = createClient<Database>(url, anonKey);
// Fully typed: data is Note[]
const { data, error } = await supabase
.from('notes')
.select('id, body, created_at')
.order('created_at', { ascending: false });
TypeScript errors at compile time if you reference a column that doesn't exist.
Authentication
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
});
// Sign in
const { data: session } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password',
});
// OAuth (Google, GitHub, etc.)
await supabase.auth.signInWithOAuth({ provider: 'github' });
The client stores and auto-refreshes the JWT in localStorage (browser) or memory (Node).
Feature comparison
| Feature |
Supabase |
Firebase |
| Database |
Postgres (full SQL) |
Firestore (NoSQL) |
| Auth providers |
20+ OAuth + email/SMS |
10+ OAuth + email/SMS |
| Realtime |
Postgres changes (CDC) |
Firestore listeners |
| Storage |
S3-compatible |
GCS-backed |
| Functions |
Deno Edge Functions |
Cloud Functions (Node) |
| Type gen |
Built-in CLI |
Manual or community tools |
| Self-host |
Yes (Docker) |
No |
How to start
- Create a project at supabase.com — free tier is generous for dev.
supabase init and supabase link --project-ref <ref> to connect CLI to the remote.
- Write migrations in
supabase/migrations/. Enable RLS on every table.
supabase gen types typescript after each schema change.
- Use
SUPABASE_ANON_KEY client-side; SUPABASE_SERVICE_ROLE_KEY only in server code.
Common mistakes
Forgetting to enable RLS. New tables are open to all authenticated users by default. Enable RLS immediately and write policies before you expose the table.
Using the service-role key in the browser. It bypasses every RLS policy. Keep it strictly server-side.
Direct schema edits in the dashboard. Any changes not in a migration file will be overwritten on the next db push. Always use migrations.
Not handling error from queries. Supabase returns { data, error } — check error !== null before using data.
What to skip
- Custom auth from scratch — Supabase Auth handles email, magic links, OAuth, and SMS OTP. Use it.
- Building your own realtime layer — Supabase Realtime exposes Postgres change streams; subscribe to
supabase.channel().
- Fetching entire tables — always use
.select() with explicit columns and add .limit() to avoid expensive full scans.
FAQ
Is Supabase free to start?
The free tier includes 500 MB database, 1 GB storage, and 50,000 monthly active users. Paid plans start at ~$25/mo.
Can I run Supabase self-hosted?
Yes — the full stack is open source on GitHub. The CLI's supabase start is effectively a local self-hosted instance.
How do I run migrations in CI/CD?
Use supabase db push --db-url $PROD_DB_URL in your pipeline, or integrate with GitHub Actions using the official action.
Does Supabase support full-text search?
Yes — use Postgres tsvector and tsquery natively, or enable the pg_trgm extension for fuzzy search. See how to add full-text search in 2026.
Where to go next