Environments provide a way to manage sets of environment variables and secrets for different stages of a project's lifecycle. They are the mechanism through which configuration values in config.json are resolved at runtime, both locally and on the platform.
This document covers the data model, CLI commands, resolution logic, and the workflows for both remote-first and local-first development modes.
An environment is a named collection of key-value pairs (environment variables) stored on the platform. Each variable belongs to exactly one environment. There is no inheritance between environments — each is an independent, flat set of variables.
Every project is created with two branches (main and dev) and three environments:
| Environment | Purpose | Mapped to |
|---|---|---|
development |
Local development via cli dev. Contains values that work on a developer's machine (localhost URLs, local database, debug settings). |
Not mapped to a branch — mapped to local execution. |
preview |
Deployed preview environments. Contains values for hosted preview infrastructure. | dev branch and all other non-production branches (via wildcard). |
production |
Live, user-facing deployment. Starts empty and is populated when the project is ready to go live. | main branch. |
All three default environments cannot be deleted or renamed.
The key distinction: development is for running locally, preview is for deploying remotely. A developer on the dev branch uses development variables when running cli dev on their machine, and preview variables when their code is deployed as a preview on the platform.
A project branch is a forked copy of the project's infrastructure running independently on its own URL. It is a first-class platform concept — not a Git concept. There are three ways a project branch gets created:
- From the dashboard — the user creates a branch directly in the platform UI. No Git involvement.
- Via GitHub integration — a Git branch push triggers the creation of a corresponding project branch (named the same by default) through GitHub webhooks.
- From the CLI — the user creates a project branch directly via CLI commands.
We intend to have the cli dev command sync project branch creation and environment switching to the local Git workflow in remote-first dev mode, so that switching Git branches locally would automatically activate the corresponding project branch and reload the environment. The details of this behavior are still being finalized.
Every project starts with two project branches: main and dev.
Users can create additional environments (e.g., staging, qa, testing) for specialized workflows. Custom environments behave identically to the defaults — they are independent sets of variables with no special relationship to other environments.
Each project branch resolves to a single deployed environment. The mapping is configured in config.json:
{
"environments": {
"production": "main",
"preview": "*"
}
}
This is the default configuration for new projects. The dev project branch (and any other non-production branch) maps to preview via the wildcard. Users can add custom mappings as the project grows:
{
"environments": {
"production": "main",
"staging": "staging",
"preview": "*"
}
}
The key is the environment name, the value is the project branch name or "*" for the wildcard (catch-all). The wildcard entry defines the default environment for any project branch not explicitly listed. If no wildcard is defined, unmapped branches fall back to preview.
The mapping is evaluated top-to-bottom; first explicit match wins, wildcard is always last. A project branch can only map to one environment.
Note: development does not appear in the branch mapping. It is not a deployment target — it is exclusively for local execution via cli dev.
Environment variables fall into two categories. Both live in the same environment, use the same CLI commands, and appear in the same dashboard — the difference is in how they are created and referenced in config.
The platform knows its own config schema. Every config key that requires a secret or environment-specific value has a canonical environment variable name derived from the config path. For example:
| Config path | Canonical variable |
|---|---|
auth.external.google.client_id |
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID |
auth.external.google.secret |
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET |
db.pooler.default_pool_size |
SUPABASE_DB_POOLER_DEFAULT_POOL_SIZE |
The user does not need to write env() for these. The config block simply declares the feature:
{
"auth": {
"external": {
"google": {
"enabled": true
}
}
}
}
The platform knows that enabling Google auth requires SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID and SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET, and resolves them from the environment automatically.
When a feature is enabled (via the dashboard or the CLI), the platform automatically creates the required variables as empty entries in the current environment, with the appropriate type (standard or secret). The CLI prompts the user to fill them in:
Google OAuth requires 2 variables:
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID
Value: 1234567890.apps.googleusercontent.com ✓
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET
Value (hidden): ••••••••••••• ✓
Stored as secret.
✓ Added to "development" environment.
When the CLI encounters an enabled feature with missing variables (e.g., during cli dev), it warns with actionable guidance:
Warning: auth.external.google is enabled but missing required variables:
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET
Set them with:
cli env set SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID "your-value" --env development
cli env set SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET "your-value" --env development --secret
Or add them to supabase/.env.local for local development.
The platform schema marks certain config fields as sensitive (e.g., auth.external.google.secret, any field containing keys, tokens, or passwords). These fields must come from an environment variable — either via implicit binding or explicit env() reference. If the CLI detects a raw value in a sensitive field, it fails with a clear error:
Error: auth.external.google.secret is a sensitive field and cannot be hardcoded in config.json.
Set it with:
cli env set SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET "your-value" --env development --secret
Or add it to supabase/.env for local development:
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET=your-value
This prevents accidental secret leaks through config.json, which is committed to Git. All secrets live in .env files (gitignored) or on the platform.
Non-sensitive fields can be hardcoded in config normally:
{
"db": {
"pooler": {
"default_pool_size": 10
}
}
}
For non-sensitive fields, the user has three options:
- Hardcode in config —
"default_pool_size": 10. Simple, committed to Git, works everywhere. - Implicit binding — omit the value, the platform resolves from
SUPABASE_DB_POOLER_DEFAULT_POOL_SIZEif set in the environment. - Explicit
env()—"default_pool_size": "env(MY_POOL_SIZE)"for cases where the value should vary per environment.
For a platform config value like auth.external.google.secret, the resolved value is determined by (first match wins):
- Canonical environment variable (
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET) resolved via the standard resolution chain (OS env →.env.local→.env). env()override in config — if the user writes"secret": "env(MY_CUSTOM_NAME)", that variable name is used instead of the canonical one (see below).
If none of the above produce a value and the feature is enabled, the CLI warns about the missing variable.
For values the platform doesn't know about — third-party service keys, application-specific config, custom feature flags — the user explicitly references environment variables using the env() syntax in config:
{
"functions": {
"my-function": {
"env": {
"OPENAI_API_KEY": "env(OPENAI_API_KEY)",
"FEATURE_FLAG_V2": "env(FEATURE_FLAG_V2)"
}
}
}
}
The user controls the naming and is responsible for setting these values in the environment.
In rare cases, a user may want a platform config key to read from a non-canonical variable name. The env() syntax serves as an escape hatch:
{
"auth": {
"external": {
"google": {
"enabled": true,
"client_id": "env(MY_GOOGLE_ID)",
"secret": "env(MY_GOOGLE_SECRET)"
}
}
}
}
This overrides the implicit binding — the platform will look for MY_GOOGLE_SECRET instead of SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET. Most users will never need this.
When running cli env pull, both types appear in the same file, grouped for clarity:
# Pulled from "development" environment
# auth.external.google
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID=1234567890.apps.googleusercontent.com
# Secrets excluded: SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET
# User variables
OPENAI_API_KEY=sk-abc123
FEATURE_FLAG_V2=true
| Mode | Config example | When to use |
|---|---|---|
| Hardcoded (non-sensitive only) | "default_pool_size": 10 |
Static config values safe to commit to Git |
| Implicit (recommended for sensitive) | "enabled": true + values in environment under canonical names |
Standard workflow — zero config boilerplate |
Explicit env() |
"secret": "env(CUSTOM_NAME)" + values in environment under custom names |
Edge cases requiring non-canonical names |
Every variable is encrypted at rest on the platform. There is no separate "secrets" storage — all variables live in the same system. The distinction is a flag on the variable, not a separate mechanism.
- Can be read, written, listed, and pulled.
- Visible in the dashboard and via
cli env list. - Included when running
cli env pull.
- Write-only after creation. The value cannot be read back from the dashboard, the API, or the CLI.
cli env listdisplays the key but shows[secret]as the value.- Excluded from
cli env pull— they never land in a local.envfile automatically. - Useful for production API keys, signing keys, and other high-sensitivity values.
A variable is marked as secret at creation time and cannot be converted back to standard. To "unsecret" a variable, delete it and recreate it as standard. Secrets are created through:
cli env set --secret— explicitly marks a variable as secret when setting it.- Interactive seeding — when seeding one environment from another, variables that are already secret in the source remain secret in the target.
- Schema auto-classification — for platform variables, the CLI auto-classifies based on
"x-secret": truein the config schema (e.g.,auth.external.google.secretis automatically created as a secret).
There is no file-based annotation or interactive prompt during push. Secrets should never flow through .env files — they are set directly on the platform via cli env set --secret or through the dashboard.
supabase/
├── config.json # project configuration, uses env() and implicit bindings
├── .env # pulled from "development" environment, gitignored
├── .env.local # personal overrides, gitignored, never synced
└── .gitignore # includes .env*
All .env* files are gitignored. There is only ever one .env file — it represents a snapshot of the development environment (or whichever environment was explicitly pulled). There are no .env.production, .env.preview, etc. files sitting on disk.
The working environment file. It is either:
- Generated by
cli env pull(remote-first) — defaults to pulling fromdevelopment, or - Created and maintained manually by the user (local-first).
Personal overrides that are never pushed to the platform and never shared with teammates. This is where a developer puts truly machine-specific values. With the development environment providing team-agreed local defaults, .env.local should rarely be needed — it's for edge cases like a personal API key or a non-standard local port.
Resolution differs between local development and deployed environments.
When the CLI encounters env(DATABASE_URL) in config.json or resolves a platform variable, the value is determined by (first match wins):
- OS environment variables — so CI/CD pipelines, Docker, and shell overrides work naturally.
.env.local— personal overrides, never synced..env— pulled from thedevelopmentenvironment or manually maintained.
On the platform, local files are not involved. The resolution is:
- Branch-specific override for the variable in the mapped environment (if one exists for the current branch).
- Base environment variable in the mapped environment.
┌─────────────────────────────────────────────────────────────────┐
│ LOCAL DEVELOPMENT (cli dev) │
│ │
│ config.json │
│ auth.external.google.secret │
│ │ │
│ ▼ │
│ ┌─ Canonical variable: SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET │
│ │ (or env() override, or hardcoded value in config) │
│ │ │
│ │ Resolved via: │
│ │ 1. OS environment ─── e.g. export in shell │
│ │ 2. .env.local ─────── personal overrides (rare) │
│ │ 3. .env ───────────── pulled from "development" │
│ │ │
│ └─ Final value │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DEPLOYED (platform) │
│ │
│ Project branch: feature-x → Environment: preview │
│ │
│ SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET │
│ 1. Branch override (feature-x) ─── if exists │
│ 2. Base value (preview) ────────── fallback │
│ │
│ Final value injected at runtime │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ development preview production │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ localhost │ │ hosted │ │ live │ │
│ │ URLs │ │ preview │ │ user- │ │
│ │ debug keys │ │ infra │ │ facing │ │
│ │ test data │ │ URLs │ │ values │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ cli dev deployed deployed │
│ (local machine) previews to production │
│ (dev branch, (main branch) │
│ feature branches, │
│ dashboard branches) │
│ │
└─────────────────────────────────────────────────────────────────────┘
# List all environments for the current project
cli env list-environments
# Create a custom environment, optionally seeding it from an existing one
cli env create <n> [--from <source_environment>]
# Seed with interactive review (see below)
cli env create <n> --from <source_environment> --interactive
# Delete a custom environment (production, preview, development cannot be deleted)
cli env delete <n>
# Seed an existing environment from another (e.g., populating production from preview)
cli env seed <target> --from <source> [--interactive]
cli env create --from and cli env seed --from both support seeding, but serve different purposes: create makes a new environment and optionally seeds it, while seed populates an existing environment (such as the default production which already exists but starts empty).
When seeding one environment from another, values often need to change — a development database URL is not the same as a production one. The --interactive flag (also available in the dashboard) walks the user through each variable:
Seeding "production" from "preview" (14 variables):
DATABASE_URL = "postgres://preview-db:5432/app"
[K]eep / [E]dit / [S]kip? e
New value: postgres://prod-db:5432/app ✓
API_ENDPOINT = "https://api.preview.example.com"
[K]eep / [E]dit / [S]kip? e
New value: https://api.example.com ✓
LOG_LEVEL = "debug"
[K]eep / [E]dit / [S]kip? e
New value: warn ✓
STRIPE_KEY = [secret]
[E]nter new value / [S]kip? e
New value (hidden): ••••••••••••• ✓
Stored as secret.
ANALYTICS_ID = "UA-12345"
[K]eep / [E]dit / [S]kip? k ✓
... (9 more)
Created "production" with 13 variables (1 skipped).
For secret variables from the source environment, the value cannot be displayed or copied — the user must enter a new value or skip the variable entirely.
Without --interactive, all variables are copied as-is (secrets included) and the user can edit them afterward with cli env set. This is useful for environments that share most values with the source (e.g., a staging environment seeded from preview).
All variable management commands operate directly on the platform. They require a linked project.
# Set a variable on a specific environment
cli env set <KEY> <value> --env <environment>
cli env set <KEY> <value> --env <environment> --secret
# Set a branch-specific override
cli env set <KEY> <value> --env <environment> --branch <branch>
# Unset (delete) a variable
cli env unset <KEY> --env <environment>
cli env unset <KEY> --env <environment> --branch <branch>
# List all variables for an environment (includes branch overrides)
cli env list --env <environment>
If --env is omitted, the CLI defaults to:
developmentwhen running locally (no deployment context).- The active environment based on the current branch mapping when a deployment context is available.
If the project is not linked, the command fails with an error.
# Pull the development environment (default for local work)
cli env pull
# Pull a specific environment
cli env pull --env <environment>
Behavior:
-
Defaults to
developmentwhen no--envis specified. This is the expected workflow — developers pull thedevelopmentenvironment for local work. -
Writes/overwrites
supabase/.envwith the resolved set of standard variables. -
When pulling a deployed environment (
preview,production, or custom), branch-specific overrides for the current branch are resolved — the.envfile contains final values, not layers. -
Secret variables are excluded. A comment is appended listing the excluded secret keys so the developer knows what to add in
.env.local:# Pulled from "development" environment # # Secrets excluded (add to .env.local if needed): # SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET # STRIPE_KEY # auth.external.google SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID=1234567890.apps.googleusercontent.com # User variables DATABASE_URL=postgres://localhost:5432/app API_URL=http://localhost:3000 FEATURE_FLAG_V2=true -
If
supabase/.envalready exists, it is overwritten without merge. Pull is a full replacement.
# Push .env contents to the development environment (default)
cli env push
# Push to a specific environment
cli env push --env <environment>
# Push from a specific file
cli env push --file .env.staging --env staging
# Push without confirmation prompt
cli env push --env development --yes
# Show what would change without applying
cli env push --env development --dry-run
# Remove remote variables not present in the local file
cli env push --env development --prune
Behavior:
-
Parse the local
.envfile (or the file specified with--file). -
Fetch the current base variables for the target environment from the platform.
-
Compute a diff and display it:
Pushing to "development" environment: + NEW_VAR = "hello" (add) ~ DATABASE_URL = "postgres://…" (changed) = API_ENDPOINT (unchanged, skipped) ! STRIPE_KEY (secret on remote, skipped) - OLD_VAR (remove, only with --prune) 2 additions/changes, 1 removal, 1 secret skipped. Continue? [y/N] -
On confirmation, send a single bulk upsert request to the platform API.
Design decisions:
- Push defaults to
developmentwhen no--envis specified, matching pull behavior. - Push always operates on base values. Branch-specific overrides cannot be set via push — they must be set individually with
cli env set --branch. This prevents accidentally turning base values into branch-scoped ones. - Without
--prune, push only adds and updates — it never deletes remote variables. This is the safe default. - With
--prune, variables present on the remote but absent from the local file are deleted. The diff clearly shows removals before confirmation. - Variables marked as
secreton the remote are skipped entirely. The diff shows! KEY (secret on remote, skipped). Push never overwrites secrets — to update a secret, usecli env set --secret. - New variables added via push are always created as standard. To create a secret, use
cli env set --secretdirectly. .env.localis never pushed. Only.env(or the file specified with--file) is used as the source.
A variable within a deployed environment (preview, production, or custom) can optionally have overrides scoped to a specific project branch. The base value applies to all project branches mapped to that environment, and a branch override takes precedence for a specific project branch only. This avoids creating a full custom environment when only a few values need to differ.
Each override is scoped to a single project branch. If the same override is needed on multiple branches, it must be set separately for each.
Note: Branch-specific overrides do not apply to the development environment, since it is not mapped to any project branch.
A team has three project branches all mapping to preview. Two of them need a different API endpoint:
# Base value — applies to all project branches mapped to preview
cli env set API_URL "https://preview.example.com" --env preview
# Project branch-specific overrides
cli env set API_URL "https://feature-x.example.com" --env preview --branch feature-x
cli env set API_URL "https://feature-y.example.com" --env preview --branch feature-y
The third project branch (feature-z) gets the base value automatically.
preview environment:
API_URL = "https://preview.example.com"
└─ feature-x = "https://feature-x.example.com"
└─ feature-y = "https://feature-y.example.com"
DATABASE_URL = "postgres://preview-db:5432/app"
STRIPE_KEY = [secret]
cli env pull --env preview resolves the correct value for the current project branch. If the current project branch is feature-x and a branch override exists for API_URL, the pulled .env contains the override value. The user doesn't need to think about layering — the pulled file always contains final resolved values.
cli env push sets base values only. Branch-specific overrides must be set individually with cli env set --branch.
# Remove a project branch-specific override (the base value remains)
cli env unset API_URL --env preview --branch feature-x
| Scenario | Recommendation |
|---|---|
| A project branch needs 1–3 different values | Branch-specific override |
| A long-lived project branch (staging, QA) needs a broadly different config | Custom environment |
| A developer needs machine-specific overrides | .env.local (no platform involvement) |
This is the standard workflow when the user has an existing project on the platform.
┌──────────────────────────────────────────────────────────────────┐
│ Platform (source of truth) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ development │ │ preview │ │ production │ ··· │
│ │ DB=local │ │ DB=preview │ │ DB=prod │ │
│ │ API=local │ │ API=prev │ │ API=prod │ │
│ └──────┬──────┘ └─────────────┘ └─────────────┘ │
│ │ │
└─────────┼────────────────────────────────────────────────────────┘
cli env pull (default)
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Local │
│ supabase/.env (pulled from development) │
│ supabase/.env.local (personal overrides, rarely needed) │
│ │
│ cli dev → reads .env.local → .env → runs local services │
└──────────────────────────────────────────────────────────────────┘
Typical day-to-day:
cli env pullto get the latestdevelopmentvariables.cli dev— everything works with localhost values. No overrides needed for most developers.- If a variable needs to change for the team, use
cli env setto update it indevelopment, or edit.envandcli env push. - Deployed previews and production use their own environments — no interaction with local files.
The user is working locally without a hosted project. The platform is not involved yet.
┌─────────────────────────────────────────────┐
│ Local only │
│ supabase/.env (manually created) │
│ supabase/.env.local (personal overrides)│
│ supabase/config.json (uses env() syntax) │
└─────────────────────────────────────────────┘
Everything works except platform sync:
env()resolves from.env.local→.env→ OS environment.cli devruns services with the correct variables.cli env pull/cli env push/cli env setfail with:Error: No linked project. Run "cli link" first.cli env list-environments,cli env create,cli env deletealso fail (environments are a platform concept).
When the user links or deploys for the first time:
cli link # or cli deploy
-
The CLI detects an existing
supabase/.envfile. -
It prompts:
Found local environment variables in supabase/.env.Push them to the "development" environment? [y/N] -
On confirmation, the
.envcontents are pushed to thedevelopmentenvironment via the bulk upsert API (all variables are created as standard). -
The developer explicitly sets any secrets on the platform:
cli env set SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET "value" --env development --secret cli env set STRIPE_KEY "sk_live_abc123" --env development --secret -
From this point on,
pullandpushwork normally.
When the user is ready to set up deployed environments, they seed preview from development and adjust the values for hosted infrastructure:
cli env seed preview --from development --interactive
Later, when going to production:
cli env seed production --from preview --interactive
This creates a natural progression: development → preview → production, each seeded from the last with values adjusted interactively.
If the remote project already has variables (e.g., set up by a teammate), the CLI detects the conflict:
The "development" environment already has 12 variables on the platform.
Your local .env has 8 variables.
[O]verwrite remote with local
[K]eep remote (discard local .env)
[C]ancel
Choose: _
1. cli init
└─ Creates supabase/config.json, empty supabase/.env
2. User edits config.json, enables Google auth
3. cli dev
└─ Fails: missing required variables for auth.external.google
└─ Shows exact commands to set them
4. User adds variables to .env
└─ supabase/.env:
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID=1234...
SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET=GOCSPX-...
DATABASE_URL=postgres://localhost:5432/app
5. cli link
└─ Links to platform project
└─ Prompts: "Push .env to development?" → yes
└─ All variables pushed as standard
5b. Set secrets explicitly on the platform:
cli env set SUPABASE_AUTH_EXTERNAL_GOOGLE_SECRET "GOCSPX-..." --env development --secret
└─ Secrets are never pushed from .env — always set via cli env set --secret
6. New teammate joins:
cli env pull
└─ Gets the development .env, runs cli dev — works immediately
7. Ready for deployed previews:
cli env seed preview --from development --interactive
└─ Replaces localhost URLs with hosted preview infrastructure
└─ Marks sensitive values as secrets
8. Ready for production:
cli env seed production --from preview --interactive
└─ Provides production database, API keys, etc.
9. Feature branch needs a different API:
cli env set API_URL "https://feature-x.example.com" --env preview --branch feature-x
└─ Project branch-specific override, no new environment needed
10. Developer switches Git branch locally while cli dev is running:
└─ (Planned) CLI syncs project branch and reloads environment automatically
The CLI commands described above require the following platform API endpoints:
| Operation | Endpoint | Notes |
|---|---|---|
| List environments | GET /projects/{id}/environments |
Returns default + custom environments |
| Create environment | POST /projects/{id}/environments |
Accepts optional from for seeding |
| Delete environment | DELETE /projects/{id}/environments/{name} |
Rejects default environments |
| Seed environment | POST /projects/{id}/environments/{name}/seed |
Accepts from, interactive handled client-side |
| List variables | GET /projects/{id}/environments/{name}/variables |
Secret values returned as null |
| Bulk upsert variables | PUT /projects/{id}/environments/{name}/variables |
Accepts full set, computes diff server-side |
| Set single variable | POST /projects/{id}/environments/{name}/variables |
Accepts secret: true and optional branch |
| Delete single variable | DELETE /projects/{id}/environments/{name}/variables/{key} |
Optional branch query param |
| Pull variables | GET /projects/{id}/environments/{name}/variables?decrypt=true&branch={branch} |
Resolves overrides, excludes secrets |
The bulk upsert endpoint (PUT) is critical. It should accept an array of {key, value, secret?} objects and an optional prune: boolean flag. The server computes the diff, applies additions/updates, and optionally removes keys not present in the payload. This avoids the one-at-a-time problem that plagues Vercel's CLI.
The platform dashboard should provide a UI equivalent for all CLI operations:
- View and switch between environments. The three defaults (
development,preview,production) are always visible. - Add, edit, and delete variables. Secret variables show a masked value that cannot be revealed.
- Create and delete custom environments, with the option to seed from an existing one (including interactive review).
- Seed an existing environment from another, with an inline UI for reviewing and editing each variable.
- Edit the branch-to-environment mapping (equivalent to editing the
environmentsblock inconfig.json). - View and manage branch-specific overrides, clearly distinguished from base values.
The dashboard is an equal citizen to the CLI — not a secondary interface.
It falls back to the wildcard ("*") mapping. If no wildcard is configured, it defaults to preview. A project branch always resolves to exactly one deployed environment.
Yes. For example, all feature branches mapping to preview is the default behavior. Multiple project branches sharing an environment means they share the same base variables (though they can have branch-specific overrides) — this is expected.
Yes. Environments are independent. ANALYTICS_KEY might exist in production but not in development. If env(ANALYTICS_KEY) is referenced in config.json and the value is missing, the CLI should warn at startup rather than fail silently.
The .env parser must handle multi-line values (using quotes), comments, and empty lines. Use an established parsing library rather than a custom regex.
Variable expansion (e.g., DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@localhost) is not supported in .env files to keep behavior predictable. Each value is treated as a literal string. Composition should happen in config.json using multiple env() calls if needed.
Edge Functions previously had a separate secrets management system (supabase secrets set/list/unset, supabase/functions/.env). The unified environments system replaces this entirely. The bridge is the env field in the functions config block, which declares which variables from the global environment system a function can access.
Old (supabase secrets) |
New (unified cli env) |
|---|---|
supabase secrets set KEY=value |
cli env set KEY value --env <environment> [--secret] |
supabase secrets set --env-file .env |
cli env push (for standard vars) + cli env set --secret |
supabase secrets list |
cli env list --env <environment> |
supabase secrets unset KEY |
cli env unset KEY --env <environment> |
supabase/functions/.env |
supabase/.env (global, from development environment) |
The supabase secrets command group is removed. All variable management goes through cli env.
The env field in a function's config block declares which variables from the global environment the function can access at runtime:
{
"functions": {
"payment-webhook": {
"env": {
"STRIPE_SECRET_KEY": "env(STRIPE_SECRET_KEY)",
"STRIPE_WEBHOOK_SECRET": "env(STRIPE_WEBHOOK_SECRET)"
}
},
"ai-assistant": {
"env": {
"OPENAI_API_KEY": "env(OPENAI_API_KEY)"
}
}
}
}- Keys = variable names the function sees via
Deno.env.get() - Values =
env()references resolved from the active environment - Key can differ from source —
"API_KEY": "env(OPENAI_API_KEY)"makes the function seeAPI_KEYwhile the environment storesOPENAI_API_KEY - Functions can only access variables declared here plus the platform defaults
This is a security improvement over the old system: functions no longer have blanket access to all secrets. Each function declares its dependencies explicitly.
Local (cli dev):
The CLI resolves each env(VAR_NAME) in the function's env block using the standard local resolution chain: OS env → .env.local → .env. The resolved values are injected into the function's runtime.
Deployed:
The platform resolves each env(VAR_NAME) from the mapped environment (e.g., preview, production). Branch-specific overrides apply as usual.
Edge Functions automatically receive the following platform variables without needing to declare them in env:
SUPABASE_URLSUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYSUPABASE_DB_URL
These are injected by the platform and are not user-configurable. They do not appear in any environment and cannot be overridden via env().
If a function has no env block in config.json, it receives only the platform defaults. This is the secure default — no user variables leak into functions that don't declare them.
If a function declares "STRIPE_KEY": "env(STRIPE_KEY)" but STRIPE_KEY is not set in the active environment, the CLI warns at startup:
Warning: functions.payment-webhook.env references missing variables:
STRIPE_KEY (from env(STRIPE_KEY))
Set it with:
cli env set STRIPE_KEY "your-value" --env development --secret
This is consistent with how missing platform variables are handled elsewhere — warn with actionable guidance rather than fail silently.
| Concept | Decision |
|---|---|
| Environments model | Flat, independent sets — no inheritance |
| Default environments | development, preview, and production — cannot be deleted |
development environment |
For local execution only (cli dev). Not mapped to a branch. Team-shared local defaults. |
preview / production |
For deployed environments. Mapped to branches via config.json. |
| Sharing between environments | Copy/seed at creation time (with interactive review), no live links. Natural progression: development → preview → production. |
| Branch-specific overrides | Supported on deployed environments — set per variable per project branch, resolved automatically on pull |
| Variables | Platform variables (implicit binding) + user variables (env() syntax) |
| Secrets | A flag on a variable, not a separate system. Set explicitly via cli env set --secret. Platform variables auto-classified from config schema. Never pushed from .env — always set directly on the platform. |
| Local files | .env (pulled from development) + .env.local (personal), both gitignored |
| Source of truth | Platform (remote-first) or .env file (local-first) |
| Sync model | pull/push default to development. Pull = full replace, push = diff + upsert (base values only) with optional prune |
| Branch mapping | Configured in config.json, maps project branch names to environments. Wildcard fallback to preview. development is not in the mapping. |
| Resolution (local) | OS env → .env.local → .env (from development). Planned: cli dev will sync Git branch switches with project branch activation and environment reload. |
| Resolution (platform) | Branch override → base environment variable |
| Edge Functions | env block in config.json declares per-function variable access. Replaces supabase secrets. Functions only see declared env() variables + platform defaults (SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL). |
| API design | Bulk upsert endpoint to avoid one-at-a-time limitations |