Supabase
Status: 🟩 COMPLETE (🟦 LIVING — Supabase ships new features monthly) Last updated: 2026-06-20 Plain-English tagline: A complete backend in a box — Postgres database + login system + file storage + realtime + APIs — without managing any servers yourself.
In plain English
Imagine you want to build a webapp. To make it real, you need:
- A database to remember things (users, posts, scores)
- A login system so users can sign up and come back
- A way to store files (profile pictures, uploaded documents)
- A way to push updates in real time when something changes (chat, live scores)
- APIs so your frontend can read and write that data
Without Supabase, you’d have to set up and run each of those things yourself, on servers you rent, with security and backups and updates and monitoring you maintain. It would take weeks, and you’d be a database administrator before you wrote a single line of app code.
Supabase gives you all of the above, already running, behind a friendly dashboard, with a generous free tier. You sign up, click “new project,” wait a minute, and you have a production-grade backend.
Supabase calls itself “the open-source Firebase alternative.” That’s a reasonable shorthand — but unlike Firebase, Supabase is built on Postgres (a standard SQL database). That means: no proprietary lock-in. If you ever leave Supabase, you can export your data as standard Postgres and run it elsewhere. That portability is a big deal.
Why it matters
For a solo developer or small team in 2026, the Next.js + Supabase + Vercel trio is the fastest, cheapest, most production-ready way to ship a real webapp. St Mark’s Bible Quest is built on exactly this stack. So are tens of thousands of other live apps.
The free tier covers projects with up to ~50,000 monthly active users and 500MB of data. The paid tier ($25/month) covers far more than most apps will ever need. Many startups hit Series A revenue on Supabase before they outgrow it.
Crucially, Supabase doesn’t lock you in: it’s an open layer on top of standard Postgres. You can self-host the whole thing if you want.
What’s in the box
Supabase is really six products under one dashboard:
1. Database — managed Postgres
Every Supabase project comes with its own Postgres database. Full Postgres, with all extensions, no proprietary flavour. You design tables, run SQL, create indexes — everything you’d do with raw Postgres, but with a nice UI and automated backups.
You interact with it three ways:
- The Table Editor in the Studio (spreadsheet-like UI for editing data)
- The SQL Editor in the Studio (write raw SQL queries)
- From your app via the SDK (
@supabase/supabase-js)
2. Auth — full user management
A complete authentication system. Out of the box you get:
- Email/password sign-up + login
- Magic links (passwordless — user clicks a link in their email)
- Social login (Google, GitHub, Apple, Facebook, Twitter, Discord, Slack, LinkedIn, and ~20 more providers)
- Phone/SMS OTP (one-time passwords)
- Passkeys (Face ID, Touch ID, Windows Hello, hardware keys — added in 2025, mature in 2026) — passwordless and phishing-resistant
- MFA (multi-factor authentication via TOTP or SMS)
- Custom SAML / OIDC providers — connect any standards-compliant identity provider (added 2026)
Users are stored in a special auth.users table. When a user logs in, Supabase issues a JWT that your app uses to identify them on subsequent requests. The JWT contains the user’s ID (auth.uid() in SQL).
3. Storage — file uploads
Buckets of files (images, videos, documents), with permission rules. Files can be public (anyone with the URL can view) or private (only allowed users). Image transformations (resize, crop, format conversion) happen on-the-fly via URL parameters.
4. Realtime — live updates
Subscribe to database changes from the client. When a row in a watched table changes, every subscribed client gets an instant notification. Perfect for chat, live dashboards, collaborative editing, presence (“who’s online right now”).
supabase
.channel("messages")
.on("postgres_changes", { event: "INSERT", schema: "public", table: "messages" }, (payload) => {
console.log("New message:", payload.new);
})
.subscribe();5. Edge Functions — serverless TypeScript
Custom server-side code that runs at the edge (close to users). For things like webhook handlers, third-party API integrations, AI inference, or anything that needs server-only secrets. Written in Deno-flavoured TypeScript.
6. Auto-generated APIs
Supabase reads your database schema and automatically generates a REST API (PostgREST) and GraphQL API. You don’t write API code — you just query the database via the SDK and it generates the appropriate HTTP requests under the hood.
The two keys (the most important thing to get right)
Every Supabase project has two API keys — and the difference between them is the #1 security concept to understand.
| Key | Where it’s used | What it can do |
|---|---|---|
anon key | In your browser/client code — safe to expose publicly | Whatever your RLS policies allow it to do, in the context of the currently-logged-in user |
service_role key | ONLY on the server — NEVER in client code | Bypasses all Row-Level Security — full admin access to the entire database |
The anon key is fine to bake into your frontend JavaScript. It gets restricted by RLS — without RLS policies, it can’t read anything; with RLS policies, it can read only what the policies allow.
The service_role key is the master key. Leaking it is a catastrophic security failure. It must only ever be:
- Set as a server-side environment variable (no
NEXT_PUBLIC_prefix!) - Used in Next.js Server Components, Server Actions, Route Handlers, or Edge Functions
- Never logged, never sent in client responses, never committed to Git
In Next.js terms:
- âś…
SUPABASE_SERVICE_ROLE_KEY→ server-only, secret - ❌
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY→ catastrophically wrong; this would expose admin access to every visitor
Row-Level Security (RLS) — the killer feature
RLS deserves its own entry, but it’s so central to using Supabase safely that you need the basics here.
Without RLS, every row in your database is readable by anyone with the anon key. With RLS, you write rules in SQL that say “you can SELECT this row only if these conditions are true.” The database enforces the rule on every query, regardless of where the query came from.
A typical rule:
-- Allow users to read only their own profile rows
CREATE POLICY "Users can read their own profile"
ON profiles FOR SELECT
USING (auth.uid() = user_id);After that policy, even if a hacker gets the anon key and writes a query for “all profiles,” they’ll only get back rows where user_id matches whoever they’re logged in as.
The default Supabase posture is: enable RLS on every table that contains user data, and write policies that allow only the access you intend. Tables without RLS enabled are an active security risk.
The Studio — the dashboard
Every Supabase project has a web dashboard at app.supabase.com/project/<id>. The main panels:
| Panel | What it’s for |
|---|---|
| Table Editor | Browse, edit, and create tables in a spreadsheet UI |
| SQL Editor | Write and run SQL queries; save them as snippets |
| Authentication | View users, sign-up settings, providers, email templates |
| Storage | Manage file buckets, view files |
| Edge Functions | Deploy and inspect serverless functions |
| Realtime | Configure which tables broadcast changes |
| Database | Schema, indexes, replication, backups, extensions |
| Reports | Usage, query performance, cache hit rates |
| Logs | Real-time logs from your database, auth, storage, functions |
| API | The auto-generated REST and GraphQL endpoints |
| Settings | Project URL, API keys, region, custom domain, etc. |
Using Supabase from your app
The SDK is @supabase/supabase-js. Install it:
npm install @supabase/supabase-jsInitialize a client (in a Next.js app, typically in lib/supabase.ts):
// lib/supabase-client.ts — for use in Client Components
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);Then in components:
// SELECT
const { data, error } = await supabase
.from("posts")
.select("*")
.eq("published", true)
.order("created_at", { ascending: false })
.limit(10);
// INSERT
const { error } = await supabase
.from("posts")
.insert({ title: "Hello world", body: "..." });
// UPDATE
const { error } = await supabase
.from("posts")
.update({ title: "New title" })
.eq("id", postId);
// DELETE
const { error } = await supabase
.from("posts")
.delete()
.eq("id", postId);The SDK is essentially a fluent SQL builder. Every chain produces a single REST request to the auto-generated API. RLS policies are enforced on every request.
Server-side usage in Next.js
For Server Components, Server Actions, and Route Handlers in Next.js App Router, use the @supabase/ssr helper package which handles session cookies correctly:
// lib/supabase-server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function getSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { /* handler — see official docs */ } }
);
}There is also a newer @supabase/server SDK (released 2026) designed specifically for server-side usage with cleaner ergonomics.
Local development
You can run Supabase entirely on your laptop for development, with the CLI:
npm install -g supabase
supabase init
supabase startThis spins up Postgres, Auth, Storage, Realtime, Studio — everything — in Docker containers. You can develop and test against a local instance, then push schema changes to your hosted project. This is hugely useful for solo development: free, fast, offline-capable.
Branching — preview environments for your database
Just like Vercel creates a preview deployment for every Git branch, Supabase branching creates a preview database for every Git branch (or, as of 2026, for any branch you create directly in the Supabase dashboard — the no-Git workflow is now the default).
You can experiment with schema changes, test migrations, try new features — all against an isolated copy of your data — and merge when ready. This is the feature that makes Supabase safe for serious teams.
Migrations — versioned schema changes
When you add a column or create a table, the change should be recorded as a migration so you can apply it to staging, production, and any future environments consistently.
The Supabase CLI generates migrations from local schema diffs:
supabase migration new add_avatar_column
# edit the generated SQL file
supabase db push # apply to the linked remote projectMigrations are plain SQL files. They live in supabase/migrations/ in your repo. Commit them to Git — your schema history travels with your code.
Pricing (as of mid-2026)
| Tier | Cost | What you get |
|---|---|---|
| Free | $0 | 1 project active, 500MB database, 1GB storage, 50K monthly active auth users, ~2GB egress. Projects pause after 1 week of inactivity. |
| Pro | $25/month | 8GB database, 100GB storage, 100K MAU, 250GB egress, no pausing, daily backups, branching, log retention |
| Team | $599/month | More headroom + SOC 2 compliance, custom SLA, priority support |
| Enterprise | Custom | For very large workloads |
Free is great for prototypes. The jump to Pro at $25/month is what most real apps need; very few solo projects exceed it.
A concrete example: from zero to a working app
The shortest path from “I have an idea” to “I have a working app with users and data”:
# 1. Create the Next.js app
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app
# 2. Install the Supabase SDK
npm install @supabase/supabase-js @supabase/ssr
# 3. Go to app.supabase.com → create a new project. Wait ~1 minute.
# 4. Copy the project URL and anon key from Settings → API.
# 5. Create .env.local:
# NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxx.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
# 6. In the Supabase SQL Editor, create a table:
# CREATE TABLE posts (
# id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
# title text NOT NULL,
# body text,
# created_at timestamptz DEFAULT now()
# );
# ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
# CREATE POLICY "Anyone can read posts" ON posts FOR SELECT USING (true);
# 7. In your Next.js page:// app/page.tsx
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export default async function HomePage() {
const { data: posts } = await supabase.from("posts").select("*");
return (
<main>
<h1>Posts</h1>
{posts?.map((p) => (
<article key={p.id}>
<h2>{p.title}</h2>
<p>{p.body}</p>
</article>
))}
</main>
);
}That’s it. A working Next.js + Supabase app, reading from a real database, RLS-protected. From scratch to live data in about 10 minutes.
Common gotchas
-
Forgetting to enable RLS. New tables have RLS disabled by default. If you create a table, add data, and the
anonkey can read it freely — that’s a security hole. Always runALTER TABLE <name> ENABLE ROW LEVEL SECURITY;on tables that contain user data, then add the policies you actually want. The Supabase dashboard now nags you about tables without RLS — listen to it. -
Using the
service_rolekey in client code. Catastrophic. It bypasses all RLS — anyone with that key has admin access to the entire database. Use it only on the server, only via env vars without theNEXT_PUBLIC_prefix. -
Region mismatch causing latency. Your Supabase project lives in one region (e.g. Sydney). Your Vercel functions can run in any region. If your Vercel functions are in the US and your Supabase is in Sydney, every database query crosses the Pacific — slow. Pin your Vercel functions to the same region as your Supabase project via
vercel.json:{ "regions": ["syd1"] }(For the Bible Quest project, this is exactly what’s done — both pinned to
syd1.) -
Free-tier projects pause after a week of inactivity. A free project with no traffic for 7 days gets paused. It comes back when you visit it, but the first request is slow. For projects you want consistently warm, upgrade to Pro.
-
Email-based auth confirmations and the Supabase default email service. The free email service is rate-limited and the emails come from a Supabase address. For anything real, configure a custom SMTP provider (Resend, SendGrid, Postmark) in the Auth settings.
-
Auto-paginated queries. By default,
.select("*")returns up to 1,000 rows. If you need more, use.range(from, to)to paginate explicitly. -
select("*")is wasteful. It pulls every column. List the columns you actually need:.select("id, title, created_at"). Faster, less bandwidth, less data exposed. -
The
auth.userstable is special. You can’t add columns to it. To add per-user data (display name, avatar URL, preferences), create a separateprofilestable that has a foreign key toauth.users.idand a row-creation trigger. -
Long-running connections don’t work in serverless. Vercel functions are short-lived. Don’t try to use Supabase’s
realtimesubscriptions from a serverless function — they’ll close as soon as the function ends. Use realtime from the client (browser) instead. -
RLS policies are SQL — and SQL silently does the wrong thing if you make a logic mistake. Test policies carefully. Supabase Studio has a “Test policy” feature; use it. Don’t ship policies you haven’t actively verified work as intended.
-
Don’t forget to commit
supabase/migrations/. Your local schema state is in your laptop. Without migrations in Git, no one (including future-you) can recreate the schema.
See also
- Postgres 🟩 — what Supabase is built on
- Row-Level Security 🟩 — the security model
- Schema design đźź©
- Migrations đźź©
- Authentication vs authorization đźź©
- Secrets management 🟩 — keeping the
service_rolekey safe - Next.js 🟩 🟦 — the framework Supabase pairs naturally with
- Vercel 🟩 🟦 — region-pinning matters
- How-to: Set up a Supabase project đźź©
- How-to: Add authentication to a Next.js app (Supabase Auth) đźź©
- How-to: Enable Row-Level Security on a table đźź©
- Supabase gotchas 🟥
- Glossary: Supabase, Postgres, RLS
Sources
- Supabase docs — canonical reference
- Supabase features — product overview
- Supabase changelog — what shipped when
- Supabase branching docs
- Passkeys / WebAuthn in Supabase — passwordless auth
- Supabase + Next.js guide