# dev

> Env files, a command with the stack's env, signing keys, and the local stacks.

Source: https://bettersupabase.com/docs/cli/dev

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 [#env]

```bash
pnpm better-supabase dev env
```

`dev 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](/docs/testing) (`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](/docs/cli/doctor#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](/docs/cli#writing-files)).

```bash
pnpm better-supabase dev env --write .env.supabase.local --prefix next
```

A 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`](#prune) removes the stacks of deleted worktrees and
branches, and [`db diff`](/docs/cli/db#diff) diffs the declarative schema
without a shadow database.

Side effects: Writes files.

## run [#run]

```bash
pnpm better-supabase dev run -- next dev
```

`dev 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:

1. The running stack, through `dev run`.
2. Your shell's exported variables.
3. The framework's env files (`.env.local` and 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:

```txt
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`](/docs/cli/doctor#bs503) reports the same mismatch when you start
the app another way.

Side effects: Runs the command you pass.

## keys [#keys]

```bash
pnpm better-supabase dev keys
```

This 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 [#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 [#ls]

```bash
pnpm better-supabase dev stack ls
```

`dev 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 [#prune]

```bash
pnpm better-supabase dev stack prune --dry-run
pnpm better-supabase dev stack prune --stop-idle
```

`dev 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](/docs/cli/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 [#one-stack-per-purpose]

`db seed --apply` and [`db query`](/docs/cli/db) 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 [#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`](#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.