Local development
Env files, signing keys, typed seeds and OpenAPI files for the local stack.
env
pnpm better-supabase envenv reads supabase status and writes the URL and keys to .env.local.
It updates the variables it manages in place and keeps every other line.
The browser variables get your framework's prefix (NEXT_PUBLIC_, VITE_,
EXPO_PUBLIC_), or the one you pass with --prefix:
| Variable | From |
|---|---|
<prefix>SUPABASE_URL | API_URL |
<prefix>SUPABASE_PUBLISHABLE_KEY | PUBLISHABLE_KEY (or the legacy ANON_KEY) |
SUPABASE_SECRET_KEY | SECRET_KEY (or the legacy SERVICE_ROLE_KEY) |
SUPABASE_DB_URL | DB_URL |
SUPABASE_JWT_SECRET | JWT_SECRET, for HS256 test tokens (asUser(..., { alg: 'HS256' })) |
Values are never printed unless you pass --print. env warns when the
file isn't in .gitignore, and when your Supabase CLI only prints legacy keys.
--out <file> writes another file than .env.local (env.output in the
config sets it for every run, and env.prefix the prefix), and --from <file>
reads saved supabase status -o json output instead of running the
Supabase CLI.
The Supabase CLI's native stack ([experimental] stack = true in
supabase/config.toml, or SUPABASE_EXPERIMENTAL_STACK=1) runs without a
Docker daemon and rejects supabase status -o json. env then reads
supabase status --env instead. That output has no JWT_SECRET, so
SUPABASE_JWT_SECRET is left out, and HS256 test tokens use the CLI's default
secret.
keys
pnpm better-supabase keysThis writes an ES256 key to supabase/signing_keys.json with mode 0600.
Point [auth] signing_keys_path at it, and local tokens are then signed the way
your hosted project signs them: asymmetrically, and verifiable through
JWKS. --rotate puts a new signing key first and keeps the old ones so
existing tokens still verify. keys refuses to overwrite an existing file
unless you pass --force, and --out <file> (or keys.output in the
config) writes somewhere else.
seed
Define fixtures once, in app casing, checked against your Insert types:
import { defineSeed } from "better-supabase/testing";
import { betterSupabase } from "../src/lib/supabase/index.ts";
export const ACME = "00000000-0000-4000-8000-000000000001";
export const seed = defineSeed(betterSupabase, {
organizations: { acme: { id: ACME, name: "Acme", slug: "acme" } },
customers: {
first: {
organizationId: ACME,
name: "First customer",
metadata: { tier: "pro" },
},
},
});pnpm better-supabase seed # writes supabase/seeds/000_better_supabase.sql
pnpm better-supabase seed --check # fails in CI when the SQL is stale
pnpm better-supabase seed --apply # also inserts into the local database--entry <file> and --out <file> override seed.entry and
seed.output. --apply connects to $DATABASE_URL or $SUPABASE_DB_URL, or to the local stack;
to seed another database, pipe its connection string in with
--db-url-stdin.
- Tables are inserted parents first, following foreign keys.
- Missing columns use their defaults, and every insert is
on conflict do nothing, so a seed can run twice. - Unknown tables and columns are type errors. A column you leave out that
has no default fails in the editor, not at
db reset. - Add the file to
[db.seed] sql_pathsinsupabase/config.toml.seedprints the line when it's missing, and it never overwrites a file it didn't write.
Tests import the same rows: seed.rows.customers.first, and
await seed.insert(sql) in a beforeAll. Node loads seed.ts directly,
so use .ts extensions in its relative imports.
openapi emit
pnpm better-supabase openapi emit --checkThis imports openapi.entry (default src/lib/openapi.ts), which exports
the createOpenApi document as openapi or
default (or a function that returns it), and writes openapi.json.
--check writes nothing and fails when the file is out of date; like
spec check, it compares the text and ignores line
endings. --entry <file> and --out <file> override openapi.entry and
openapi.output.
openapi emit is kept for existing projects. spec
writes the same document from a defineApi module, plus any other OpenAPI
version, AsyncAPI and Arazzo, with overlays, a cache and validation. The
openapi config key is a deprecated alias of specs: when specs is
unset, spec reads openapi.entry and openapi.output too.
Last updated on