Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
82cc401
feat(cli): add supabase config validate (CLI-2866)
Coly010 Nov 10, 2026
e15c211
fix(cli): harden config validate masking and env-reference handling
Coly010 Nov 10, 2026
a1fc13e
fix(cli): mask nested and URL-embedded secrets in config validate
Coly010 Nov 10, 2026
03d6642
fix(cli): print config values in validate only when provably safe
Coly010 Nov 10, 2026
f459bb8
fix(cli): parse URLs before showing them in config validate
Coly010 Nov 10, 2026
2df0c09
fix(cli): show keys and positions only in config validate frames
Coly010 Nov 11, 2026
a01fe58
fix(cli): report config validate problems by key and position only
Coly010 Nov 11, 2026
25407c1
fix(cli): tighten config validate remote overrides, key paths and loc…
Coly010 Nov 11, 2026
1c63810
Merge remote-tracking branch 'origin/develop' into columferry/cli-286…
Coly010 Nov 11, 2026
44d27f9
fix(cli): validate duplicate remotes and handle dotted and bracketed …
Coly010 Nov 11, 2026
c56ab3d
fix(cli): validate every remote block and keep unresolved env() from …
Coly010 Nov 12, 2026
1cefeef
Merge remote-tracking branch 'origin/develop' into columferry/cli-286…
Coly010 Nov 12, 2026
93200c8
fix(cli): scope config validate env() suppression to the keys each ch…
Coly010 Nov 12, 2026
ece2fc4
fix(cli): declare enabled gates on config file reads and provider checks
Coly010 Nov 12, 2026
70dd3ec
Merge remote-tracking branch 'origin/develop' into columferry/cli-286…
Coly010 Nov 12, 2026
2c27b74
Merge remote-tracking branch 'origin/develop' into columferry/cli-286…
Coly010 Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/bench/workloads.pr.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{ "id": "init", "args": ["init"], "cwdSetup": "none" },
{ "id": "migration-new", "args": ["migration", "new", "bench"], "cwdSetup": "init" },
{ "id": "config-convert", "args": ["config", "convert"], "cwdSetup": "init" },
{ "id": "config-validate", "args": ["config", "validate"], "cwdSetup": "init" },
{
"id": "gen-types",
"args": ["gen", "types", "--lang", "typescript", "--project-id", "replayfixtureprojref"],
Expand Down
92 changes: 92 additions & 0 deletions apps/cli/docs/supabase/config/validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# supabase-config-validate

Checks the project config file (`supabase/config.toml`, `config.json` or `config.jsonc`) and reports every problem in one pass, each with its key path and `file:line:column`. It never modifies a project file and never contacts the Supabase API.

## What is checked

1. The file parses (TOML or JSONC).
2. The schema version is one this CLI reads.
3. Only one config file applies: `config.json` and `config.jsonc` together is an error, and `config.toml` next to a JSON file is reported as shadowed (the JSON file is the one used).
4. The env files load and no removed `SUPABASE_*` variable is set, so `env(NAME)` references and overrides resolve.
5. Removed keys, unknown keys (with a `did you mean` suggestion when one is close), values of the wrong type, enum or range, and missing required keys.
6. The validations `supabase start` applies, such as a missing provider secret or an unreadable `content_path` file.
7. Every `[remotes.<name>]` block, merged over the base config. Its problems carry the remote name.

Stages 1 to 4 stop the run when they fail, because later checks cannot be computed; the report says where it stopped (`complete: false`, `stopped_at` in JSON). A schema check that keeps finding dependent failures stops at `schema_limit` with the failures found so far. A remote `project_id` set by a `SUPABASE_REMOTES_*` variable is reported against that variable, not the file. Everything from stage 5 on is reported together.

## Secrets

The report shows key paths and `file:line:column`, never source text or values, in text and JSON alike (see [ADR 0034](https://github.com/supabase/cli/blob/develop/docs/adr/0034-config-diagnostics-print-no-source-text.md)). A value that comes from a `SUPABASE_*` variable is reported against its key, not a line.

## `env()` references

An unset `env(NAME)` is a warning when the section it sits in is enabled (the nearest `enabled` is not `false`), and the errors it would cause are not reported. Optional placeholders written by `supabase init` stay quiet. Warnings never change the exit code.

## Two config files

`config.json` and `config.jsonc` together is an error. `config.toml` next to a JSON file reports `shadowed_file`.

## Example

```text
✗ db.major_version unsupported Postgres version
supabase/config.toml:9:17
help: use one of 13, 14, 15, 17

✗ 1 error in supabase/config.toml
```

```json
{
"schema_version": 1,
"valid": false,
"complete": true,
"file": "supabase/config.toml",
"format": "toml",
"remotes": [],
"errors": [
{
"code": "invalid_value",
"severity": "error",
"key": "db.major_version",
"path": "db.major_version",
"reason": "unsupported Postgres version",
"help": "use one of 13, 14, 15, 17",
"line": 9,
"column": 17,
"end_line": 9,
"end_column": 19
}
],
"warnings": [],
"message": "supabase/config.toml has 1 problem"
}
```

Problem codes: `syntax`, `file_conflict`, `schema_version`, `removed_key`, `unknown_key`, `invalid_type`, `invalid_enum`, `invalid_value`, `missing_required`, `remote_project_id`, `env_file`, `removed_env`, `env_unresolved`, `deprecated_key`, `shadowed_file`, `unrecognized_schema`. Remotes that could not be checked are listed in `skipped_remotes`. Failures before a report exists (no config file, bad `--workdir`) use the generic `{"_tag":"Error",...}` envelope.

The exit code is `0` when valid and `1` when there are errors. `-o/--output` is not supported; use `--output-format`.

## CI and pre-commit

```yaml
- run: supabase config validate
```

```yaml
repos:
- repo: local
hooks:
- id: supabase-config-validate
name: supabase config validate
entry: supabase config validate
language: system
files: ^supabase/config\.(toml|jsonc?)$
pass_filenames: false
```

```sh
supabase config validate
supabase config validate --workdir path/to/project
supabase config validate --output-format json
```
12 changes: 6 additions & 6 deletions apps/cli/docs/tracing-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ the timing ranges, API calls per command, and the slowest phases. It reports:
The baseline API calls per workload on `develop` (a project-scoped command
also refreshes the linked-project cache, so it makes two calls):

| Workload | API calls |
| ------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `version`, `help`, `init`, `migration-new`, `config-convert` | 0 |
| `projects-list` | 1: `GET /v1/projects` |
| `gen-types` | 2: `GET /v1/projects/{ref}/types/typescript`, `GET /v1/projects/{ref}` |
| `functions-list` | 2: `GET /v1/projects/{ref}/functions`, `GET /v1/projects/{ref}` |
| Workload | API calls |
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `version`, `help`, `init`, `migration-new`, `config-convert`, `config-validate` | 0 |
| `projects-list` | 1: `GET /v1/projects` |
| `gen-types` | 2: `GET /v1/projects/{ref}/types/typescript`, `GET /v1/projects/{ref}` |
| `functions-list` | 2: `GET /v1/projects/{ref}/functions`, `GET /v1/projects/{ref}` |

`SUPABASE_NO_KEYRING=1` disables the keyring (`CommandCredentials.load` reports
`keyring.enabled=false`); the value `true` is not recognized. Counts come from the untimed traced
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { withEnvVar, mockCommandSettings } from "../../tests/helpers/command-moc
import { CommandPlatformApiFactory } from "../auth/command-platform-api-factory.service.ts";
import { readDbConfig } from "./db-config.read.ts";
import { resolveStorageCredentials } from "./storage-credentials.ts";
import { resolveLocalConfigValues } from "./local-config-values.ts";
import { collectLocalConfigIssues, resolveLocalConfigValues } from "./local-config-values.ts";
import { runtimeInfoLayer } from "../shared/runtime/runtime-info.layer.ts";
import { cliConfigValuesTestLayer } from "../../tests/helpers/config-values-layer.ts";

Expand Down Expand Up @@ -82,6 +82,28 @@ function failsWithL(
}).pipe(Effect.provide([BunServices.layer, BunCrypto.layer]));
}

/** Drives the collecting pipeline and asserts its first issue is the one the throwing pipeline fails with. */
function collectsFirstWithL(
overrides: Record<string, unknown>,
message: string,
document?: Readonly<Record<string, unknown>>,
) {
return Effect.gen(function* () {
const issues = yield* collectLocalConfigIssues(
baseConfig(overrides),
WORKDIR,
document,
undefined,
);
const config = baseConfig(overrides);
const thrown = yield* Effect.flip(
resolveLocalConfigValues(config, "127.0.0.1", WORKDIR, document),
);
expect(issues[0]?.message).toContain(message);
expect(issues[0]?.message).toBe(thrown.message);
}).pipe(Effect.provide([BunServices.layer, BunCrypto.layer]));
}

interface ParityScenario {
readonly name: string;
readonly toml: ReadonlyArray<string>;
Expand Down Expand Up @@ -333,9 +355,17 @@ describe("validateResolvedConfig cross-caller parity (D vs L)", () => {
Effect.gen(function* () {
yield* failsWithD(scenario.toml, scenario.message);
yield* failsWithL(scenario.overrides, scenario.message, scenario.document);
yield* collectsFirstWithL(scenario.overrides, scenario.message, scenario.document);
}),
);
}

it.effect("a valid config collects no issues", () =>
Effect.gen(function* () {
const issues = yield* collectLocalConfigIssues(baseConfig(), WORKDIR, undefined, undefined);
expect(issues).toEqual([]);
}).pipe(Effect.provide([BunServices.layer, BunCrypto.layer])),
);
});

// The `api.port` branch is L-only in the table above (D has no api section) but is shared with
Expand Down
Loading
Loading