dev
Env files, a command with the stack's env, signing keys, and the local stacks.
The dev commands set up and inspect local development: dev env writes
the env files from the running stack, dev run runs a command with them,
dev keys writes a signing key, and dev stack lists and prunes the local
stacks.
env
pnpm better-supabase dev envdev env 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.
Server code reads the plain names. The browser copies get your framework's
prefix (NEXT_PUBLIC_, VITE_, EXPO_PUBLIC_), or the one you pass with
--prefix. --prefix takes a framework name (next, vite, expo,
nuxt, astro, sveltekit), none, or the prefix itself:
| Variable | From |
|---|---|
SUPABASE_URL | API_URL |
SUPABASE_PUBLISHABLE_KEY | PUBLISHABLE_KEY (or the legacy ANON_KEY) |
<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_JWKS_URL | API_URL plus /auth/v1/.well-known/jwks.json |
SUPABASE_DB_URL | DB_URL |
SUPABASE_JWT_SECRET | JWT_SECRET, for HS256 test tokens (asUser(..., { alg: 'HS256' })) |
Only the URL and the publishable key get the prefix. The secret key, the database URL and the JWT secret never do, because a prefixed variable is bundled into client code (BS501).
Values are never printed unless you pass --write -, which prints the
variables on stdout instead of writing a file. The file is readable by
your user only (mode 0600), even when its contents didn't change. dev env
warns when the file isn't in .gitignore, and when your Supabase CLI only
prints legacy keys.
--write <file> (or its older name --out) writes another file than
.env.local (env.output in the config sets it for every run, and
env.prefix the prefix), and
--status-file <file> reads saved supabase status -o json output instead of running the
Supabase CLI. Both paths must stay inside the project root: dev env refuses an
absolute path elsewhere or one that climbs out with .., so the secret key
never lands outside the project. --dry-run and --check name the file
without showing the keys (writing files).
pnpm better-supabase dev env --write .env.supabase.local --prefix nextA separate file keeps the stack's values apart from the ones you edit. In
Next.js, .env.supabase.local is not one of the files it loads, so load it
with dev run below or a dotenv loader. If you write to .env.local, Next.js
loads it after .env.development.local and before .env.development and
.env, and a variable from the first file that sets it wins. It skips
.env.local under NODE_ENV=test.
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. dev 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.
The native stack starts one stack per checkout and branch, and each running
Postgres holds a System V shared memory segment. When segments run out,
dev stack prune removes the stacks of deleted worktrees and
branches, and db diff diffs the declarative schema
without a shadow database.
Side effects: Writes files.
run
pnpm better-supabase dev run -- next devdev run reads supabase status and starts the command after -- with the
stack's variables (the ones dev env writes) set, so the app always talks to
the stack this worktree is running. It passes the child's output and exit
code through, forwards Ctrl+C (SIGINT) and SIGTERM to it, and
--prefix picks the browser prefix the same way dev env does. Nothing is
written to disk.
Each variable is resolved in this order, first one set wins:
- The running stack, through
dev run. - Your shell's exported variables.
- The framework's env files (
.env.localand the rest). Next.js, Vite and dotenv never override a variable that is already set, so the first two win over every file.
A SUPABASE_* variable exported in your shell (left over from another
project or a hosted one) would otherwise point the app at the wrong stack.
dev run uses the stack's value and warns, naming the variable. The warning
shows only the host and port of a URL, and a sha256: fingerprint of a key,
never the key itself:
SUPABASE_URL is exported as 127.0.0.1:54321, but the running stack has 127.0.0.1:55021. run uses the stack's value.doctor reports the same mismatch when you start
the app another way.
Side effects: Runs the command you pass.
keys
pnpm better-supabase dev 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. dev keys refuses to overwrite an existing file
unless you pass --force, and --out <file> (or tooling.keys.output in the
config) writes somewhere else. --dry-run writes nothing.
Side effects: Writes files.
stack
The Supabase CLI's stack engine ([experimental] stack = true) starts one
local stack per checkout and branch. Stacks outlive the worktrees and branches
they were started for, and every running Postgres holds one System V shared
memory segment. macOS allows 32 segments by default (kern.sysv.shmmni), so a
machine with many stacks runs out, and the next shadow database fails with
could not create shared memory segment: No space left on device. dev stack ls
and dev stack prune show which stacks hold segments and remove the ones you no longer
need.
ls
pnpm better-supabase dev stack lsdev stack ls lists every stack from supabase stack list with its branch, its
project root and whether it is running. It marks the stack of this checkout,
a stack whose worktree is gone and a stack whose branch is deleted, and
counts the shared memory segments each stack's postmaster holds. The last
line shows how many segments are in use out of the system limit. The JSON
envelope has the stacks in data.stacks and the totals in
data.sharedMemory; unattributed counts segments no managed stack holds.
Side effects: Reads the local stacks.
prune
pnpm better-supabase dev stack prune --dry-run
pnpm better-supabase dev stack prune --stop-idledev stack prune destroys each stack whose worktree no longer exists or whose
branch was deleted, with supabase stack destroy, which also removes its
data. It never touches the stack of this checkout and branch.
| Flag | Effect |
|---|---|
--stop-idle | Also stops the running stacks of other branches and checkouts. Their data stays. |
--dry-run | Prints the stacks it would destroy or stop, and changes nothing. |
In agent mode, destroying a stack needs the user's consent first
(Agent mode). Stopping a stack frees its segment, and
supabase start in that checkout starts it again with its data. Run
better-supabase doctor to see the segment use as a finding (BS700).
Side effects: Stops or destroys local stacks.
One stack per purpose
db seed --apply and db query connect to the stack in
supabase/config.toml. To keep a second stack for tests or a demo, give it
its own [db] port in supabase/stacks/<name>/config.toml and pass
--stack <name> or set BSB_STACK=<name>. Without a port there, the
command reads the DB_URL from supabase status --stack <name>, and fails
with not_found when that stack isn't running. A connection string from
$DATABASE_URL or --db-url-stdin still wins. If two stacks can't run side
by side on your Supabase CLI, use one git worktree per purpose instead; each
worktree gets its own stack.
Too many stacks on macOS
The native stack engine keeps one stack per checkout and branch, and a stack
keeps running after its shell exits. Each running stack's Postgres holds a
System V shared memory segment, and macOS allows 32 of them on the whole
machine (sysctl kern.sysv.shmmni). Once they are used up, the next
supabase start fails because its Postgres can't get a segment. ipcs -m
lists the segments in use.
Stop the stacks you aren't using. dev stack ls shows every
stack on the machine with its branch and whether its worktree still exists,
and dev stack prune destroys the stacks of deleted worktrees and merged
branches (--stop-idle also stops the idle ones). supabase stop --stack-id
with a stack's id stops that stack without deleting its data. doctor
reports the segment use as BS700 before it runs out.
Last updated on