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.

KeyWhere it’s usedWhat it can do
anon keyIn your browser/client code — safe to expose publiclyWhatever your RLS policies allow it to do, in the context of the currently-logged-in user
service_role keyONLY on the server — NEVER in client codeBypasses 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:

PanelWhat it’s for
Table EditorBrowse, edit, and create tables in a spreadsheet UI
SQL EditorWrite and run SQL queries; save them as snippets
AuthenticationView users, sign-up settings, providers, email templates
StorageManage file buckets, view files
Edge FunctionsDeploy and inspect serverless functions
RealtimeConfigure which tables broadcast changes
DatabaseSchema, indexes, replication, backups, extensions
ReportsUsage, query performance, cache hit rates
LogsReal-time logs from your database, auth, storage, functions
APIThe auto-generated REST and GraphQL endpoints
SettingsProject URL, API keys, region, custom domain, etc.

Using Supabase from your app

The SDK is @supabase/supabase-js. Install it:

npm install @supabase/supabase-js

Initialize 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 start

This 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 project

Migrations 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)

TierCostWhat you get
Free$01 project active, 500MB database, 1GB storage, 50K monthly active auth users, ~2GB egress. Projects pause after 1 week of inactivity.
Pro$25/month8GB database, 100GB storage, 100K MAU, 250GB egress, no pausing, daily backups, branching, log retention
Team$599/monthMore headroom + SOC 2 compliance, custom SLA, priority support
EnterpriseCustomFor 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 anon key can read it freely — that’s a security hole. Always run ALTER 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_role key 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 the NEXT_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.users table is special. You can’t add columns to it. To add per-user data (display name, avatar URL, preferences), create a separate profiles table that has a foreign key to auth.users.id and a row-creation trigger.

  • Long-running connections don’t work in serverless. Vercel functions are short-lived. Don’t try to use Supabase’s realtime subscriptions 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


Sources