# Local development

> Env files, signing keys, typed seeds and OpenAPI files for the local stack.

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

## env [#env]

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

`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.
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](/docs/testing) (`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 [#keys]

```bash
pnpm better-supabase 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. `keys` refuses to overwrite an existing file
unless you pass `--force`, and `--out <file>` (or `keys.output` in the
config) writes somewhere else.

## seed [#seed]

Define fixtures once, in app casing, checked against your Insert types:

```ts title="supabase/seed.ts"
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" },
    },
  },
});
```

```bash
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_paths` in `supabase/config.toml`. `seed`
  prints 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 [#openapi-emit]

```bash
pnpm better-supabase openapi emit --check
```

This imports `openapi.entry` (default `src/lib/openapi.ts`), which exports
the [`createOpenApi`](/docs/standards/openapi) 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`](/docs/cli/spec), 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`](/docs/cli/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.