---
title: "Supabase: Best Practices"
slug: "supabase"
category: "backend"
tags: ["supabase", "backend", "postgres", "auth", "serverless", "pooling"]
status: "stable"
last_updated: 2026-06-16
summary: "Supabase setup, migrations, connection pooling, and key security rules for production use."
related: ["[[backend/supabase-rls]]", "[[backend/postgres]]", "[[backend/migrations]]", "[[backend/auth-sessions]]", "[[ops/secrets-and-env]]", "[[backend/prisma-pooling]]", "[[ops/postgres-prod]]"]
---

## Overview

Supabase is an open-source Firebase alternative that bundles Postgres, Auth, Storage, Realtime, and auto-generated REST and GraphQL APIs into a single managed platform. Use it when you want a Postgres-backed backend with built-in auth and row-level security without operating your own database cluster.

## Initialize and link a project with the CLI

Install the Supabase CLI, then run `supabase init` in your project root. This creates a `supabase/` folder containing `config.toml`, a seed file, and the migrations directory (Supabase docs).

```bash
npm install -D supabase   # global `npm install -g supabase` is unsupported; or: brew install supabase/tap/supabase
npx supabase init
npx supabase link         # links the local project to a hosted Supabase project
```

`supabase link` stores a project reference in `supabase/.temp/` and is required before you can push migrations remotely.

## Use `supabase start` for local development

`supabase start` boots a full local stack (Postgres, Auth, Storage, Studio, Realtime) inside Docker (Supabase docs). Run it before writing migrations or testing Edge Functions locally.

```bash
supabase start
# Outputs local API URL, anon key, and service_role key for local use only
```

Stop the stack with `supabase stop`. The local environment is ephemeral; seed data is reapplied from `supabase/seed.sql` on each fresh start.

## Manage schema changes through migrations, never through the Dashboard in production

Create a migration file with `supabase migration new <name>`, which writes a timestamped SQL file to `supabase/migrations/` (Supabase docs). Edit that file, then apply it.

```bash
supabase migration new add_posts_table
# Edit supabase/migrations/<timestamp>_add_posts_table.sql

supabase db push           # applies all pending migrations to the linked remote project
```

Direct Dashboard schema edits in production bypass version control. Use the Dashboard only to prototype locally, then capture the diff in a migration file. See [[backend/migrations]] for general migration discipline.

## Use transaction mode pooling (port 6543) for serverless workloads

Supabase provides Supavisor as its connection pooler. Serverless functions and edge runtimes open a new TCP connection per invocation; direct Postgres connections (port 5432) will exhaust the connection limit under load (Supabase docs).

**Transaction mode** (port 6543) shares a fixed pool of server-side connections across all clients. A client holds a connection only for the duration of a single transaction, then releases it. This is the correct mode for Edge Functions, Next.js API routes, and any stateless handler.

**Session mode** (port 5432) gives each client an exclusive connection for the lifetime of its session. Use it only for long-lived processes such as migration runners or ORMs that rely on prepared statements. Note: as of February 28, 2025, Supabase deprecated session mode on port 6543; port 6543 now exclusively serves transaction mode (Supabase changelog).

```
# Transaction pooler (serverless)
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres

# Session / direct (long-lived processes, migrations)
postgresql://postgres.<project-ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres
```

Transaction mode does not support prepared statements. ORMs that use prepared statements by default (including Prisma) must disable them when using port 6543. See [[backend/prisma-pooling]] for the Prisma-specific configuration.

## Never expose the `service_role` key in client-side code

Supabase projects expose a low-privilege client key and an elevated server key. The `anon` key (in the new key scheme, the `sb_publishable_...` publishable key that replaces it) is safe to ship in browser or mobile code; access is governed by Postgres [[backend/supabase-rls]] policies on the `anon` and `authenticated` roles. The `service_role` key (replaced by `sb_secret_...` secret keys in the new scheme) authorizes the `service_role` Postgres role, which carries the `BYPASSRLS` attribute, so it skips every Row Level Security policy and grants full read/write access to all tables (Supabase docs).

The `service_role` key belongs only in server-side environments: backend API handlers, Edge Functions running in a trusted server context, CI scripts. Store it as an environment variable and never commit it to source control. See [[ops/secrets-and-env]] for secret management patterns.

```ts
// Safe: anon key in a browser client
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)

// UNSAFE: service_role key must never appear in NEXT_PUBLIC_ variables
// const admin = createClient(url, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!) // do not do this
```

RLS policy detail lives in [[backend/supabase-rls]]. Auth session handling is covered in [[backend/auth-sessions]].

## Related

- [[backend/supabase-rls]]
- [[backend/postgres]]
- [[backend/migrations]]
- [[backend/auth-sessions]]
- [[ops/secrets-and-env]]
- [[backend/prisma-pooling]]
- [[ops/postgres-prod]]
