Skip to content

chore: production deploy - #7253

Open
supabase-cli-releaser[bot] wants to merge 80 commits into
mainfrom
develop
Open

supabase-cli-releaser[bot] wants to merge 80 commits into
mainfrom
develop

Conversation

@supabase-cli-releaser

Copy link
Copy Markdown
Contributor

Prashansa-K and others added 30 commits October 14, 2026 15:37
Maps `migration list`, `fetch`, `repair`, `up`, `down`, `squash` and
`new`, plus `test db` and `test new`, to the Management API operations
they call, so a scoped access token can be granted per command.

The six database commands declare `dbConfig` and the linked-project
cache under the linked target. `list`, `fetch` and `repair` default to
linked; `up`, `down` and `squash` default to local, where no request is
made. `--password` (list, repair, squash) removes the login-role call,
and `--project-ref` adds the pooler config fetch. `migration new` and
`test new` make no calls. `test db` declares the same database config as
its hidden alias `db test`.

Each command's integration tests run once through the real database
config resolver and linked-project cache and check the recorded requests
against the declarations. That covers the linked run (pooler config,
login role, project lookup for the cache), the local run (no request;
for `up`, `down`, `squash` and `test db` a login-role request is also
rejected under the default flags), `migration up --linked=false`, and
`--password` on `list`, `repair` and `squash`.

Also corrects the stale Management API routes in `migration squash`'s
`SIDE_EFFECTS.md`.

No operations without FGA permissions and no shared block changes.
`--linked=false` is exercised through the real layers only on `migration
up`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Maps the Management API permissions for the `inspect` commands: the 25
`inspect db` subcommands and `inspect report`.

All 25 `inspect db` subcommands run one shared query runner, so they
share one declaration: the database-config block (`dbConfig`) with
`linked` as the default database target. `inspect report` declares the
same block plus `--output-dir`, which makes no API call. On `--local`
and `--db-url` neither command calls the Management API. On the linked
project they fetch the pooler config (with `--project-ref`) and create a
temporary login role unless a database password is already available.
Neither command fills the linked-project cache or prompts for a project.

Tests run the real database-config resolver, record the requests it
makes and check them against the declarations. One linked `db-stats` run
is checked against all 25 `inspect db` declarations, taken from the real
command tree; `--linked=false`, an empty `--project-ref` and the
`--local`/`--db-url` no-request cases are covered separately, as is
`inspect report`.

Shared pieces: no block or model change, no `oneOf`/`is` conditions of
its own, no new operations, no change to `OPERATIONS_WITHOUT_FGA`. The
26 commands leave the pending list.

Docs: `inspect db`'s `SIDE_EFFECTS.md` had the wrong login-role and
network-ban routes and no concrete pooler route; `inspect report`'s
claimed a linked-project cache write that nothing performs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ase-oss (#7124)

## Summary

Claude-resolved `sync/develop-into-next` PRs now get approved without a
maintainer when the merge is clean in practice:

- every check except `Require fast-forward` is green on the head,
- the AI review posted a review on that exact head,
- Claude fixed or declined every finding (declining the ones the merge
did not introduce),
- no `.github/` workflow suggestions are waiting.

`sync-repair` plans an `approve` action in that state, and a new
`approve` job submits an approval for that commit as `supabase-oss`
through the `auto-approve` environment. The approval runs
`fast-forward.yml` as usual, which re-checks the approver, the head and
the checks before moving `next`.

A head moved past the reviewed commit, by a repair commit or a
maintainer push, is never re-reviewed, so it still waits for a person.
The job skips if the head moved or `supabase-oss` already approved that
commit.

This reverses the documented rule that the bot never approves sync
heads; `release-process.md` records the new accepted decision. Reviewers
should confirm the `auto-approve` environment's branch rule lets
`sync-repair` runs on `develop` deploy.

## Linked issue

- [x] I'm a Supabase maintainer.

## Checklist

- [x] The PR title follows Conventional Commits.
- [x] Tests added or updated for the change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## TL;DR

patches a few flakes:

- **native launcher pin** failed when a cache sweep briefly locked the
artifact pin. it now waits up to 2s like the owner's own pin, and a
failed launch carries the launcher's stderr.
- **native Realtime** could be marked ready by another process answering
its port. it now waits for its own bind line like Pooler, so a lost port
relaunches.
- **HostProcess argv** blocked the event loop on a slow Windows CIM
query. i've made the read async with a named guard.
- **Owner.logs resume** could read history before the first record
landed. it now waits for output first.
- **pinned ports** in the Network tests could land on RDP 3389 on
Windows. they now stay under 3000.
- **Owner.analytics** now says whether the events table or the row was
missing.

## ref:

- spotted in:
[launcher
pin](https://github.com/supabase/cli/actions/runs/38494662586/job/115627588416)
[native
Realtime](https://github.com/supabase/cli/actions/runs/38415198781/job/115357678754)
[HostProcess
argv](https://github.com/supabase/cli/actions/runs/38399326861/job/115302578574)
[Owner.logs](https://github.com/supabase/cli/actions/runs/38373564250/job/115216209193)
[pinned
ports](https://github.com/supabase/cli/actions/runs/38536558032/job/115761323171)
[Owner.analytics](https://github.com/supabase/cli/actions/runs/38232117245/job/114753854349)

---------

Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
This bumps storage from v1.82.1-r0 to v1.83.0-r0.


https://github.com/supabase/slim-services/releases/tag/storage-v1.83.0-r0

Planned after supabase/slim-services published v1.83.0-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…7123)

## Summary

Every Management API call in `apps/cli` now goes through the typed
`@supabase/api` client. Each call gets an `api.operation` span named
after its OpenAPI operation ID, so telemetry can tell which operations a
command really calls. It complements the permission mapping added in
#7105, which declares the operations each module calls.

Before this PR, a dozen call sites built URLs by hand. Those calls
skipped the span, response decoding, and the client's retry and error
handling. Some of them existed because the generated client couldn't
express what they needed, so this PR fixes the generator and client
first, then moves the call sites, then adds a lint rule so new raw URLs
fail CI.

```mermaid
flowchart LR
  subgraph before["Before"]
    direction TB
    H1["command handler"] -->|"typed call"| C1["@supabase/api"]
    H2["sso / snippets / advisors /<br/>postgres-config / db query / login …"] -->|"hand-built /v1/... URL"| X1["HttpClient"]
    C1 --> X1
    X1 --> API1[("Management API")]
  end
  subgraph after["After"]
    direction TB
    H3["every command handler<br/>and shared module"] -->|"api.v1.op(input, options?)"| C2["@supabase/api<br/>span · decode · retry"]
    C2 --> X2["HttpClient"]
    X2 --> API2[("Management API")]
    L["lint rule<br/>supabase/no-raw-management-api-url"] -. "blocks new raw URLs" .-> H3
  end
```

## What changes

### 1. Generator and client (`packages/api`)

Four gaps forced callers back to raw HTTP:

| Gap | Fix |
| --- | --- |
| Response enums were closed, so a new server value broke decoding |
Multi-value response enums are open: `"ACTIVE" \| "PAUSED" \| (string &
{})`. Single-value enums stay closed. |
| Unknown request keys were dropped without an error | Request input
decodes with `onExcessProperty: "error"`, so a typo fails as
`SupabaseApiInputError` before any request is sent |
| `const` schemas weren't generated | `const` maps to a literal |
| One client meant one host and one token | Every wrapper takes an
optional second argument: `{ baseUrl?, accessToken?, headers? }` |

The last row is what let login and SSO profile reconciliation drop raw
HTTP:

```ts
// before: login built its own request because the client had no token yet
const response = yield* httpClient.execute(
  HttpClientRequest.get(`${apiHost}/v1/profile`).pipe(
    HttpClientRequest.setHeader("Authorization", `Bearer ${token}`),
  ),
);

// after
const api = yield* makeApiClient({ baseUrl: apiHost, accessToken: token, userAgent });
const profile = yield* api.v1.getProfile();
```

```ts
// per-request override on an existing client (sso update against a reconciled profile)
yield* api.v1.updateASsoProvider(input, { baseUrl: profile.apiUrl, accessToken: profile.token });
```

**Retries now back off.** The client used to retry immediately. It now
uses exponential backoff with full jitter:

```mermaid
xychart-beta
  title "Maximum delay before each retry (ms, full jitter picks 0 to max)"
  x-axis ["retry 1", "retry 2", "retry 3", "retry 4", "retry 5"]
  y-axis "ms" 0 --> 4500
  bar [250, 500, 1000, 2000, 4000]
```

| Response | Retried for | Delay |
| --- | --- | --- |
| `429` | every method | `Retry-After` if present, otherwise backoff |
| `5xx` / transport error | `GET` `PUT` `DELETE` `HEAD` only |
`Retry-After` if present, otherwise backoff |
| other `4xx` | never | — |

`POST` and `PATCH` aren't retried on 5xx because they aren't idempotent.
A `Retry-After` longer than 10 s isn't waited out: the response goes
straight back to the caller. Each retry records
`http.request.resend_count` on the span.

A single request can override the policy with `retry: { maxRetries,
requestTimeoutMs }`:

```ts
// functions deploy runs its own rate-limit loop, so the client stays out of it
api.executeRaw(operationDefinitions.v1DeployAFunction, input, { retry: { maxRetries: 0 } });
```

| Caller | Override | Why |
| --- | --- | --- |
| `functions deploy` upload and bulk update | `maxRetries: 0` | the
command's own `429` loop is the only retry layer |
| `postgres-config update/delete` PUT | `maxRetries: 0` | a resent write
with `restart_database` restarts the database twice |
| linked-project cache fill at exit | `maxRetries: 0` | exit isn't held
up when the API is failing |
| `db query --linked` | `requestTimeoutMs: Infinity` | long queries keep
running with no client timeout, as before |
| `sso add` / `sso update` | `requestTimeoutMs: Infinity` | the server
may fetch `--metadata-url` during the request; no client timeout, as
before |

### 2. Self-validating OpenAPI overrides

`openapi-overrides.json` patches upstream spec bugs. Patches used to
fail silently once upstream fixed the bug, so stale patches piled up.
The loader now rejects:

- a `replace` without a `test` on the same path directly before it, so
the patch states what it expects upstream to say;
- a `replace` whose value already matches upstream, because the fix has
landed upstream;
- a `remove` whose target doesn't exist;
- any entry without a `$comment` explaining the upstream bug.

```jsonc
{ "$comment": "Hostnames stored before the upstream fix can lack `status`; the fix covers only new writes.",
  "op": "test",    "path": "/components/schemas/.../required", "value": ["status", "..."] },
{ "op": "replace", "path": "/components/schemas/.../required", "value": ["..."] }
```

With these checks, the overrides go from 37 entries to 25. The removed
patches were ones upstream has since fixed, plus a lenient removal of
the v2 webhook paths, which now generate normally.

### 3. Call sites moved to the typed client (`apps/cli`)

| Area | Before | After |
| --- | --- | --- |
| `sso add` / `sso update` | string URLs, hand-written response views |
`createASsoProvider`, `getASsoProvider`, `updateASsoProvider`,
per-request host and token for reconciled profiles. Structured output
still prints the server body unchanged. |
| `db query` | raw `POST /database/query` | `v1RunAQuery` |
| `postgres-config get/update/delete` | raw GET and PUT | GET keeps the
raw record (stored keys may predate the schema). PUT validates the keys
the user passed against the same strict schema the platform enforces.
Stored overrides are resent unvalidated through `executeRaw`'s
`extraBody`, so one this CLI version doesn't know is kept. The returned
record prints unchanged. |
| `snippets list/download` | raw | typed operation. `--output
json/yaml/toml` still prints the server body unchanged. |
| `db advisors` (linked) | raw | `getSecurityAdvisors`,
`getPerformanceAdvisors` |
| `config push` cost check | raw | `listProjectAddons` |
| login profile, upgrade suggestion, linked-project cache | raw | typed
calls. Each builds its own client for its host and token. |
| `projects list` / `projects create` | typed client's `executeRaw` |
unchanged: `executeRaw` keeps fields the schema doesn't model |

The shared `raw-http.ts` helper is deleted. One raw call remains, the
device-login poll under `/platform/cli/login`. That route isn't in the
Management API spec, so no typed operation exists. It now URL-encodes
its parameters and has a commented lint exception.

### 4. Lint rule: `supabase/no-raw-management-api-url`

A local oxlint JS plugin (`tools/oxlint/supabase-plugin.js`) reports any
string or template literal in `apps/cli/src` (tests excluded) that
starts a `/v1/…`, `/v2/…` or `/platform/…` path, including path
constants that are joined to a base URL later. Service-scoped paths such
as `/auth/v1/` and `/storage/v1/` don't match.

```ts
const url = `${apiHost}/v1/projects/${ref}/config/database/postgres`;
//          ^ supabase/no-raw-management-api-url: Build Management API requests with the
//            typed `@supabase/api` client (`api.v1.*`, `execute`/`executeRaw`) so the call
//            emits the `api.operation` span; do not assemble `/v1/`, `/v2/` or `/platform/` URLs by hand.
```

### 5. cli-e2e placeholder ref

Recorded fixtures used `__PROJECT_REF__` as the project ref, which the
typed client rejects as a path parameter. That kept login, upgrade
suggestion and the linked-project cache on raw HTTP. Fixtures now use
`replayfixtureprojref`, a 20-character ref that passes the check, and
the replay server no longer rewrites placeholders back and forth. This
explains the large fixture rename in the diff. Injected `429`/`5xx`
replay responses send `Retry-After: 0`, so the new backoff doesn't slow
down the suite.

## User-visible behavior changes

Most of these come from validating input locally and decoding responses
strictly:

- `postgres-config update` and `config push` reject keys and values the
platform would refuse before sending a request. An unknown `--config`
key reports `unknown Postgres config parameter(s): …` and suggests
upgrading the CLI if the parameter is new. Stored overrides are never
checked locally. An invalid value reports `invalid Postgres config value
for <key>: …`, and a bare number for a size or duration suggests adding
a unit (`work_mem=4096kB`).
- The SSO attribute-mapping file is decoded strictly, so unknown keys
are an error. The `sso` table shows only modelled fields.
- `notebooks` rejects an unknown `view`.
- `functions deploy` rejects an unknown function status. Unknown
custom-domain statuses are shown as returned.
- `snippets` output follows the spec's nullability (`description: null`
instead of a missing field).
- `db query` error output caps the response body and strips control
characters.
- Idempotent requests now retry on `5xx` and transport errors with
backoff. `429` is retried for every method, except where a command opts
out (see the overrides table above).
This bumps storage from v1.83.0-r0 to v1.83.1-r0.


https://github.com/supabase/slim-services/releases/tag/storage-v1.83.1-r0

Planned after supabase/slim-services published v1.83.1-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps analytics from v1.53.0-r0 to v1.53.1-r0.


https://github.com/supabase/slim-services/releases/tag/analytics-v1.53.1-r0

Planned after supabase/slim-services published v1.53.1-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This PR was automatically created to sync the generated `@supabase/api`
package with the latest Management API OpenAPI document.

Changes were detected in the upstream OpenAPI documents exposed by
`https://api.supabase.com/api/v1-json` and
`https://api.supabase.com/api/v2-json`.

Co-authored-by: jgoux <1443499+jgoux@users.noreply.github.com>
)

Maps `gen types`, `gen signing-key` and `gen bearer-jwt` to the
Management API operations they call, so a scoped access token can be
granted per command. `gen keys` stays pending.

`gen types` picks its target from flag values, so it declares flag
conditions instead of a database target: `--local`, `--db-url` and
`--postgrest-v9-compat` turn every call off. TypeScript calls the types
endpoint. Other languages look the project up, fall back to the preview
branch config on a 404, and reach the database through the database
config resolver (login role, pooler config) and the pooler retry. The
project picker and the linked-project cache fill are declared too.
`signing-key` and `bearer-jwt` make no calls.

Shared block change: a target-free `dbConfigPoolerFallbackLinked` for
commands that choose their target from flag values
(`dbConfigPoolerFallback` is built from the same entries), and the first
fallback context is reworded to fit every command. Dump, pull and the
db-config tests are unchanged.

`gen types` runs through the real database config resolver and
linked-project cache with the Management API recorded, for the
TypeScript, pooler, IPv6 retry, preview branch, picker, `--local=false`
and `--db-url` paths. On this command the pooler, login-role and
network-ban calls are each declared by more than one source, so removing
one source alone is not caught by a test; the grants are per operation
either way. No operations without FGA permissions. Only `typescript` and
`go` run through the recording mocks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
Maps `branches create|delete|disable|get|list|pause|unpause|update` and
`backups list|restore` to the Management API operations they can call,
so scoped PATs can be granted per command. Drift tests keep each
declaration in step with what the command does.

**Shared changes.**
- New `branchPrompt` block (`v1-list-all-branches`, required) for the
branch listing shown when no branch is given on a TTY.
- The context text of `projectRefPicker`, `linkedProjectCache` and
`branchRef` is reworded so it fits every command that includes it. No
condition or label changed.

**Blocks used.** `projectRefPicker` and `linkedProjectCache` (all ten),
`branchPrompt` and `branchRef` (delete, pause, unpause, update; `get`
uses `branchPrompt` and declares its name lookup itself),
`upgradeSuggestion` (create, update).

**Conditions.** `branches get` names the global output flags: the
branch's API keys and pooler config are read for `-o` other than
`pretty`, and with no `-o` when the output format is json or stream-json
(`--output-format`, `--agent yes`, or a detected coding agent). This is
the first declaration to name global flags in `when`.

**Findings.**
- `pause` and `unpause` call the project-level pause and restore
operations, so they need `project_admin_write`, not a branching
permission.
- `disable` needs only `branching_production_delete`.
- A 4xx on `create` or `update` triggers the upgrade-suggestion lookups
(best-effort).

**Tests.** Each command's integration test runs the project picker, the
branch picker where it applies, and the real linked-project cache, and
checks the exact request sequence. `branches get` covers each output
mode that changes its calls; the agent-detected case is simulated
through the output format.

**Not covered.** Context entries (`branchPrompt`, `branchRef`, the
cache, the agent-detected entry) are asserted by request sequence only.
`--persistent=false` is not run on every command.

**Docs.** The `branches` `SIDE_EFFECTS.md` files now give the cache path
as `<workdir>/supabase/.temp/linked-project.json`, and `get`'s doc
states when the API keys and pooler config are read. All ten docs now
list the project picker, branch picker and linked-project cache routes,
and say when the cache file is written.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
Maps the Management API operations behind `config diff`, `config pull`
and `config push`, so scoped access tokens can be given matching
permissions. This changes no command behaviour.

**Commands:** `config diff`, `config pull`, `config push` (removed from
the pending list).

**New shared blocks**
- `configTarget` (`project-target.ts`): the branch lookup for a non-ref
`--project-ref` (`v1-get-a-branch-config` for a UUID, `v1-get-a-branch`
for a name), else the project picker. Reuses `branchRef` and
`projectRefPicker`.
- `configPullRun` (`config-pull-run.ts`): the remote config read
`v2-get-project-config`, shared with top-level `pull`.

**Declarations**
- `diff`: `configTarget`, the config read, linked-project cache.
- `pull`: `configTarget`, `configPullRun`, linked-project cache.
- `push`: `configTarget`; the target probe (best-effort); the add-ons
list and config read (required); one required write per resource (api,
db settings, network restrictions, SSL enforcement, auth, storage,
webhooks), each with a context naming what triggers it; the branch
listing (best-effort); linked-project cache.

**Conditions:** `--project-ref` present gates the branch lookups. Write
triggers, the probe and the branch listing are contexts, since they
depend on `config.toml` contents and `.temp` state rather than flags.

**Worth knowing**
- `config push` always needs the add-ons read, even when nothing is
billable.
- Which writes happen depends on which sections the token can read.
- Not covered by a drift check: `push`'s cache inclusion (its project
probe shares an operation), and the write contexts, which are asserted
by request.

**Docs:** three wrong rows in `config push/SIDE_EFFECTS.md` corrected
(branch lookup parent, cost matrix order, webhooks key).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Record the Management API operations behind 20 project-data commands, so
scoped access tokens can be built from what each command actually calls:
`sso add|info|list|remove|show|update`, `domains
activate|create|delete|get|reverify`, `secrets set|unset`, `snippets
download`, `notebooks pull|push` and `storage cp|ls|mv|rm`.

Each command declares its own operations and composes the existing
`projectRefPicker`, `linkedProjectCache`, `upgradeSuggestion` (sso
commands that call the upgrade gate) and `storageCredentials` blocks. No
user-facing behaviour changes.

New shared pieces:
- `notebooksList`, `notebookRead` and `notebookCreate` blocks in
`notebooks.shared.ts`, for the calls both notebooks commands make; each
command adds its own context.
- `STORAGE_PERMISSIONS` in `storage.flags.ts`. Storage routes on the
parsed `--local` value, so its credentials and cache entries use `{
flag: "local", is: false }` rather than a database target;
`--local=false` targets the linked project.
- A `recordManagementApi` option in the storage test setup and a
`realLayers` option in the notebooks test setup, so those tests run the
real picker, cache and credentials lookup and check the recorded
requests against the declarations.

Conditions: `secrets unset` lists secrets only when no names are given,
and the notebooks entries carry contexts for the id argument, the
reconcile answers and which files exist. `exact` drift checks skip
those, so they are asserted by request sequence.

Things worth knowing:
- `sso info` makes no call of its own, but needs a token and fills the
project cache; its doc said otherwise.
- `snippets download` resolves a project ref it never sends, so it fails
unlinked even though the snippet call needs no ref.
- `storage cp` resolves credentials before its source/destination
checks, and `rm` before its confirmation.
- The `--linked`/`--local` mutual-exclusion error isn't modelled, so
storage over-declares when both are set to false.
- The experimental gate on storage isn't modelled; declarations describe
a run with the gate open.
- Dropping `linkedProjectCache` from `sso add|list|remove|update` fails
no test, because `upgradeSuggestion` also declares the project lookup.
- The `SIDE_EFFECTS.md` files for these commands had the wrong
linked-project cache path and conditions, and were missing the picker
and cache-fill routes; corrected in the last commit.

No `OPERATIONS_WITHOUT_FGA` changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
#7170)

Maps the Management API operations behind the projects, organizations
and linking commands, so scoped access tokens can be given matching
permissions. This changes no command behaviour.

**Commands:** `projects api-keys|create|delete|list`, `orgs
create|list`, `link`, `unlink`, `bootstrap`, `services` (removed from
the pending list).

**New shared block:** `organizationPicker` (`projects.prompt.ts`): the
organization listing made when no organization ID is given.

**Declarations**
- `projects api-keys`: the keys read, with `reveal=true` when `--reveal`
is set; the project picker and the cache fill.
- `projects create`: the create call, the organization listing (via the
picker, when `--interactive` allows a prompt), the cache fill.
- `projects delete`: the delete call and a project listing for its own
picker (it ignores the env and `.temp` ref, so it doesn't reuse the
shared picker), the cache fill.
- `projects list`, `orgs create|list`: their single call; `list` also
fills the cache for a linked ref.
- `link`: project, API keys (`reveal=true`), storage and pooler reads,
the branch listings, the picker, the cache fill. `unlink` makes no API
calls.
- `bootstrap`: organization listing (always, since it passes no org ID),
project create, API keys, health, storage and pooler reads, cache fill.
The pooler read is required because the migration push depends on the
pooler URL it saves on IPv4-only networks. No login call is declared:
with no token the command fails before the login runs.
- `services`: the linked project's API keys and project reads (both
best-effort), cache fill.

**Conditions:** `--reveal` is tested by value. `--interactive` is
optional, so it is declared as two entries (absent, and present with "is
true" in the context). Interactive-terminal, config and argument-shape
prerequisites are contexts.

**Worth knowing**
- `bootstrap`'s browser login is unreachable when no token resolves; the
platform-API layer fails first.
- `link --password` and `SUPABASE_DB_PASSWORD` have no effect.
- Not covered by a drift check: `link`'s cache inclusion and its second
branch-listing entry (a required entry for the same operation shadows
them), and `bootstrap`'s pooler entry (asserted from the handler).

**Docs:** wrong rows corrected in the projects, bootstrap and link
`SIDE_EFFECTS.md` files (environment variable name, create body, org
listing, bootstrap request body and cache-fill attribution, the
`--password` row).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Follow-up to #7129. CLI e2e jobs now set the Test workflow's wall time.

- Split the `@supabase/cli-e2e` suite into two shards; it was the single
longest job.
- The e2e sequencer packs shards by estimated wall time: concurrent
files share half the `e2e` project's workers, because they contend for
one Docker daemon, and exclusive files add their full duration.
- Drop the `actions/cache` + `go build` steps in e2e jobs;
`supabase#build` already builds or restores `@supabase/cli-go` through
turbo.
- The `gen types` e2e readiness probe connects over TCP. The Postgres
entrypoint's init server listens only on the Unix socket, so a socket
probe could pass before the final server started and the seed then
failed mid-restart.
- macOS port tests run on Blacksmith macOS runners; GitHub-hosted macOS
runs took up to several times as long.

Measured over repeated CI runs against develop after #7129:

| | develop after #7129 | This PR | Change |
| --- | --- | --- | --- |
| Test workflow wall time (median) | 3.9 min | 2.75 min | about 30%
faster |
| Slowest CLI e2e job | 3.3 min | about 2.5 min | about 25% faster |
| macOS port tests job | 2.1–8.5 min | 0.7–0.9 min | 3–10x faster |
| Files on the Docker-heavy CLI shard | 32 | 10 | |

Required check names are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…7139)

## Summary

`supabase db diff --from linked --to linked` failed with `password
authentication failed for user "cli_login_postgres"` when no database
password was available (no `--password`, `SUPABASE_DB_PASSWORD`, or
`supabase/.env`). Each linked side called the resolver separately,
minting two temporary login roles; the second mint invalidated the first
side's credentials.

The explicit-mode `linked` endpoint is now resolved once and reused for
both sides, so a run issues a single `POST
/v1/projects/{ref}/cli/login-role`. Runs with a password configured are
unchanged.

Also corrects routes in the `SIDE_EFFECTS.md` files for `db diff`, `db
pull`, `db dump`, `migration squash`, `inspect db`, and `test db`: temp
login role is `POST /cli/login-role`, pooler fallback is `GET
/config/database/pooler`, and ban listing is `POST
/network-bans/retrieve`.

## Linked issue

Supabase maintainer change; no linked issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Declares the Management API permissions for the project network
commands: `vanity-subdomains` (get, check-availability, activate,
delete), `ssl-enforcement` (get, update), `encryption` (get-root-key,
update-root-key), `network-bans` (get, remove), `network-restrictions`
(get, update) and `postgres-config` (get, update, delete). Each leaf
composes the project-ref picker and the linked-project cache.

**Shared change:** `postgresConfigRead` and `postgresConfigWrite` are
new blocks in `postgres-config.shared.ts`, next to the helpers that
issue the calls (`postgres-config` reaches the API through `executeRaw`
on the typed client, so the drift matcher resolves them). `get` uses the
read block, `update` the read block (only when
`--replace-existing-overrides` is false) plus the write block, and
`delete` both.

**Conditions declared**
- `network-restrictions update`: `is` on `--append` selects the POST
`/apply` or the PATCH.
- `ssl-enforcement update`: the PUT is declared once per switch
(`--enable-db-ssl-enforcement`, `--disable-db-ssl-enforcement`), each
with `is: true`.
- `vanity-subdomains activate` and `check-availability`: the call is
declared for `--desired-subdomain` present and for `oneOf: [""]`,
because the handlers send an empty value to the API. Both also compose
`upgradeSuggestion`.

**Findings**
- `v1-get-pgsodium-config` is a GET that needs `project_admin_write`,
and `check-availability` is a POST that needs `vanity_subdomain_write`.
- `postgres-config` makes no restart call and has no delete endpoint:
`delete` is a GET followed by a PUT, and `--no-restart` only shapes the
PUT body.
- `network-bans remove` fills the linked-project cache even when an IP
is invalid, while `network-restrictions update` and `ssl-enforcement
update` validate before resolving the ref and fill nothing.
`postgres-config update` only checks that each `--config` value is a
`key=value` pair before resolving; an unknown key is rejected after the
ref resolves and the GET, so the cache is still filled. `encryption`
resolves the ref outside the cache-fill scope.
- The SIDE_EFFECTS docs pointed the cache at
`~/.supabase/<workdir-hash>/`, described a `.supabase/config.json` that
nothing reads, and omitted the picker and cache-fill routes. They now
match the code.

**Not covered**
- The experimental gate is not modelled; declarations describe a run
whose gate is open.
- `ssl-enforcement update` over-declares when both switches are true, a
run that fails before any call.
- Picker, cache and upgrade-lookup entries carry a context, so they are
checked by request sequence rather than by the exact drift check.
Dropping `linkedProjectCache` from vanity `activate` or
`check-availability` fails no test, because `upgradeSuggestion` also
declares `v1-get-project`.
- `--switch=false` and `--desired-subdomain=` are exercised with parsed
values, not through the flag parser.
- No new entries in `OPERATIONS_WITHOUT_FGA`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Maps which Management API operations the `functions` and `compute`
commands call, and under which flags, so scoped tokens can be granted
the right permissions. The mapping is recorded in each command's
declaration, and integration tests check it against the requests the
handlers actually make. There is no user-facing change.

**Commands (12):** `functions` list, delete, download, deploy, new,
serve; `compute` list, status, delete, logs, push, new.

**Blocks used:** `projectRefPicker` and `linkedProjectCache` on every
command that calls the API.

**New shared blocks:**
- `functionsDownload` in `src/shared/functions/download.ts`, next to
`downloadFunctions`, so `pull` can compose it later.
- `computeRead` in `src/shared/compute/compute-api.ts`, used by status,
delete, logs and push.

**`is` conditions declared:**
- `functions deploy`: `use-api`, `use-docker`, `no-verify-jwt` and
`prune` select the API path, the Docker path, the JWT list lookup and
prune.
- `compute logs`: `follow`.
- `compute push`: `no-wait`.

**Notes**
- `functions download` lists `--use-api`, `--use-docker` and
`--legacy-bundle` as flags that don't change the call set. Under
`--legacy-bundle` the Go binary makes its own list, get and body calls,
which aren't recorded.
- `compute delete` marks its lookup best-effort: it carries on to the
DELETE after a 403 on the GET.
- `compute logs` needs the `analytics_logs_read` permission, unlike the
other compute commands.
- `functions new`, `functions serve` and `compute new` make no API calls
and declare none. Their layers provide no API client, so a call fails
type-checking.
- The context-only entries (deploy create, update, bulk update, prune
delete and the API-path list; download's get and list; logs' history and
compute lookup) carry a `context` and are asserted through explicit
request sequences, not `exact` drift checks.
- `--no-verify-jwt=false` on deploy over-declares the API-path list
call, and `compute logs --tail 0` can't be expressed as a condition.
- The registry unit test's "undeclared command in a gated tree" example
moves from `compute list` to `stack list`.
- `OPERATIONS_WITHOUT_FGA`: no change.

The `SIDE_EFFECTS.md` commit corrects the cache-fill wording and adds
the missing picker and cache rows for the functions and compute
commands.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
… --all (#7177)

`supabase stop --no-backup` no longer exits 1 when a Docker CLI older
than 23.0 talks to a newer daemon.

`docker volume prune --all` is a client-side flag. Docker CLI 20.10
doesn't have it, and newer CLIs reject it when the negotiated API
version is below 1.42. The gate only read the daemon's API version, so a
20.10 client against a 25.0 daemon was handed `--all`: the containers
were already gone, the volume prune failed, and `stop` reported
`StopVolumePruneError`. The gate now reads `{{.Client.APIVersion}}
{{.Server.APIVersion}}` and passes `--all` only when both are 1.42 or
later. A request negotiated below 1.42 already prunes named volumes
without the flag, so nothing is left behind.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This bumps realtime from v2.143.4-r0 to v2.144.0-r0.


https://github.com/supabase/slim-services/releases/tag/realtime-v2.144.0-r0

Planned after supabase/slim-services published v2.144.0-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Maps the permissions of `login`, `whoami` and the local-only commands
(`logout`, `feedback add|delete`, `issue bug|docs|feature`, `completion
bash|fish|powershell|zsh`, `telemetry enable|disable|status`), so scoped
tokens can tell which of them need Management API access.

- `login` composes the existing best-effort `loginProfile` block: the
profile lookup only stitches the telemetry identity, and login succeeds
when it fails.
- `whoami` declares `v1-get-profile` as required, since the profile is
its whole output.
- The other 13 leaves can't reach the Management API, so they declare no
operations and classify their flags as having no API effect. They get no
drift checks, which would be vacuous.
- `v1-get-profile` has no FGA permission in the spec, so it becomes the
first `OPERATIONS_WITHOUT_FGA` entry (a shared test change).
- Tests: `login` now runs the real login API layer against a recording
HTTP mock (`--token` path, a 403 on the profile lookup, and the browser
flow); `whoami` gains drift checks on its happy and 401 paths.

Notes:
- `login`'s device-flow poll (`/platform/cli/login/{sessionId}`) isn't
in the spec, so it can't be declared.
- `bootstrap` can reach the same profile call when no token resolves;
that path isn't changed here.
- `whoami -o …` fails before its required call, so the declaration
over-declares that error run.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Maps the top-level `pull` command to the Management API operations it
calls.

`pull` composes the config-pull, linked-project cache, database config
and function-download blocks directly, because its sub-commands'
declarations describe different flags and gating. The database and
function steps are gated on `--dry-run` being false, since a dry run
stops after the target and config reads. `pull` also declares its own
pooler-config entry: its database steps always get the resolved ref, so
the pooler fetch happens without `--project-ref`.

Findings: a bootstrap run resolves the database twice, so it fetches the
pooler config and mints a login role twice; step failures are isolated
but `pull` still fails, so every step call is required.

Untested conditions: a declined confirmation (it over-declares, like
`--dry-run`), a failing step under the real layers, and a branch-name
target under the real layers. `dbConfigLinked`,
`dbConfigPoolerFallbackLinked` and the own pooler entry overlap, so
dropping one alone fails no test. No blocks without a real-layer test,
no best-effort label turned required, no `OPERATIONS_WITHOUT_FGA`
entries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…ns (#7179)

Records which Management API operations each of these commands calls, as
`withPermissions` declarations checked by the existing completeness and
drift tests.

- `init`, `stack destroy|list|logs|prepare|restart|start|status|stop`
and legacy `start` and `stop` make no Management API calls; each
declares no operations and classifies every own flag, hidden ones
included. The aliased `start|status|stop` in the stack tree are covered
by the same declarations.
- Legacy `status` declares one best-effort `v1-list-all-branches`
through the existing `branchTarget` block, with a context naming the
linked-ref, cached-parent and token prerequisites.
- Legacy `start` gets a drift check on the requests its bucket-seeding
test records, and legacy `status` gets HTTP-level 200 and 403
branch-lookup tests checked against the declaration.
- No new blocks, conditions or `OPERATIONS_WITHOUT_FGA` entries.
Untested: the stack `start` drift check and the `SUPABASE_PROJECT_ID=''`
case for `status`.
- The `declaredPermissions` test now asserts `Declared` for `stack list`
instead of `Undeclared`, because that leaf is no longer undeclared. A
second test asserts that every pending command still in the tree is
reported as `Undeclared`, and fails once the pending list is empty, at
which point it can be deleted.
- Reworded the `status` runtime-layer comment, which claimed `status`
makes no Management API calls.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## Summary

**TL;DR:** A new `Flaky check` workflow runs one commit on many fresh
runners with retries off and reports every test that both passed and
failed. It runs nightly on `develop`, weekly for e2e, and on demand
through the `run-flaky-check` PR label, so PRs that fix flaky tests can
show the fix holds.

```mermaid
flowchart LR
  T["nightly develop / weekly e2e<br/>run-flaky-check label / dispatch"] --> P["plan: commit, suites, N runs"]
  P --> M["N fresh runners per suite<br/>retries off, JUnit output"]
  M --> R["report: flaky / always failing<br/>runs without results"]
  R --> O["artifact + job summary<br/>PR comment"]
```

### Why

Flaky tests surface one at a time in unrelated PRs and get retried away.
There is no way to ask "is this commit stable?" or to show that a
flaky-test fix actually holds. `stack-stress.yml` covers two stack
scenarios only.

### What changed

- `flaky-check.yml` runs:
  - nightly: unit + integration ×10 on `develop`
  - weekly: CLI e2e (4 shards + `cli-e2e`) and stack whole-stack e2e ×3
- `run-flaky-check` label: unit + integration ×10 on the PR merge
commit, plus a `focused` suite that repeats the tests the PR affects
(`vitest --changed` against the merge base, `--repeats=4`, ×10 runs)
- manual dispatch: any ref, suites, run count, repeats, and file filter
- The label is removed when the run starts, so re-adding it starts
another run.
- A report job classifies each test as flaky (mixed results) or always
failing. It also lists runs that produced no usable results: timeouts,
crashes, missing JUnit reports, or missing uploads. Each failed
`--repeats` execution counts separately.
- The report goes to the job summary, a `flaky-check-report` artifact
(`report.md`, `report.json`), and one sticky PR comment. The report job
fails when it finds anything.
- `.github/MAINTAINERS.md` documents the triggers and the `focused`
caveats.

### Linked issue

None; maintainer change. The `run-flaky-check` label already exists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Local Studio logs pages fail with "Backend error" on the default
Postgres analytics backend (#7155). Logflare 1.52.0 and later cast
endpoint query parameters such as `@project` to `jsonb` when translating
to Postgres, so every logs query fails with `invalid input syntax for
type json` (Logflare/logflare#4158).

- Pins `analytics` back to Logflare 1.50.15 (slim `v1.50.15-r1`), the
last release before the regression, in the stack catalog and both
service Dockerfiles.
- Adds an `UPGRADE_HOLDS` entry for `analytics` to the slim release
planner so the bump bot can't upgrade it again. Hotfix revisions of
1.50.15 still go through. Remove the entry once Logflare ships a fix.

The per-minute log chart still fails on the Postgres backend because
`IFNULL` is never translated (Logflare/logflare#4159); that has been the
case in every Logflare version.

A local database first created by Logflare 1.53.x (CLI 2.121.0) keeps
its Oban tables only in `_analytics`, which 1.50.15 doesn't read. On
those databases the analytics alerting and event-touch workers restart
in a loop and log `relation "public.oban_jobs" does not exist`. Log
ingestion and queries are unaffected, and `supabase stop --no-backup`
clears it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This bumps storage from v1.83.1-r0 to v1.83.3-r0.


https://github.com/supabase/slim-services/releases/tag/storage-v1.83.3-r0

Planned after supabase/slim-services published v1.83.3-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Closes the command permission mapping: every command leaf now declares a
permission status, so the pending list is gone.

The six commands that still hand off to the bundled Go binary (`db
branch create|delete|list|switch`, `db remote changes`, `gen keys`)
declare `unmapped` with reason `go-delegated`. The completeness test now
requires a declaration on every leaf, and a second check fails if a path
on the go-delegation allowlist stops being a real leaf declared
`unmapped`. The pending list and the registry test that depended on it
are removed; the `Undeclared` lookup result stays for command groups.

Findings: `unmapped` records how these commands are implemented, not
what they need. Reading the Go source, the four `db branch` leaves make
no Management API calls, `gen keys` makes one
(`V1GetPostgrestServiceConfig`), and `db remote changes` runs the same
diff as `db diff` against the linked database. All six could be mapped
later, which would let the `unmapped` status and its allowlist go.

Untested conditions: the six `unmapped` commands make no claim, so
nothing checks them. No blocks without a real-layer test, no best-effort
label turned required, no `OPERATIONS_WITHOUT_FGA` entries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…d stops (#7184)

Two native stacks started at the same time could end up running their
Logflare backends on the same private port. Each native backend port was
held only by a placeholder listener, and that listener closes right
before the backend is spawned. Logflare takes a few seconds to boot and
then binds with `SO_REUSEPORT`, so another stack's scan could pick the
same port in that window, and both Logflares bound it. The kernel then
split connections between the two stacks, so a request carrying one
stack's API key could reach the other stack's Logflare and get a 401.
This was the intermittent `401` from the analytics marker in the native
whole-stack lifecycle e2e.

- Native backend ports (10000–19999) are now claimed in the per-user
port registry (`ports.sqlite`). The claim is taken before the
placeholder binds and released only after the backend process stops, so
other stacks' scans skip the port for the whole handoff.
- A claim whose stack has no running owner, or whose registration is
gone, is taken over. A same-stack claim not held by the current owner
process is treated as left by a dead owner.
- A registry error fails the port scan straight away instead of being
retried as a bad port.
- `CatalogOptions.claimNativePort` is required, and the owner wires it
to the registry.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This bumps realtime from v2.144.0-r0 to v2.144.2-r0.


https://github.com/supabase/slim-services/releases/tag/realtime-v2.144.2-r0

Planned after supabase/slim-services published v2.144.1-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This PR was automatically created to sync API types from the
infrastructure repository.

Changes were detected after refreshing `apps/cli-go/api/v1-openapi.yaml`
from the latest spec from infrastructure, and `apps/cli-go/pkg/api` is
regenerated from it. The snapshot diff is the upstream API change and is
worth reading: it is marked `linguist-generated`, so GitHub collapses it
behind a "Load diff" click rather than hiding it.

Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com>
7ttp and others added 10 commits October 19, 2026 15:11
…7217)

## TL;DR

brings the `src/cli` area under the effect lint, tests included

## whats introduced?

- `src/cli/**` joins the allow list, and `docs`, `output` and the top
level files follow in the next PR of this stack.
- shell completion runs as one effect with a named span, taking the
clock and the run id from `Clock` and `Crypto`, so its output, exit
codes and telemetry event stay the same and the capture still lands
before exit
- `isValidGoRfc3339` checks dates through `DateTime`, with a few new
test cases for `gen bearer-jwt --exp`
- the cli tests move to `it.effect`, `it.live` and a new
`withTempCliProject` helper

## ref

- related: CLI-2742
…LI-2742) (#7218)

## TL;DR

brings the `docs` and `output` areas under the effect lint, with the top
level `main.ts` and `globals.d.ts`
## whats introduced?

- `docs/**`, `output/**`, `main.ts` and `globals.d.ts` join the allow
list, and `output` and the top level files needed no changes
- `readDocsContent` walks the docs through the `FileSystem` and `Path`
services with a named span, keeps a leading BOM, and wraps YAML and
shape failures in a typed `DocsExamplesError` that keeps the original
error
- the generator script rethrows the original fs, YAML or shape error, so
the generated spec, error messages and exit codes stay the same, but an
fs failure such as a missing `docs/supabase` or
`templates/examples.yaml` no longer prints a source code frame or stack,
since async `FileSystem` errors carry none
- the walk matches develop on any tree that is not changing during the
walk, except a directory `stat` cannot reach, such as one inside a
readable but not searchable directory or past the path length limit,
which is now skipped, or fails on its read when named `.md`, since the
`FileSystem` service has no dirent types
- the docs tests move to a lazy loader, scoped temp dirs and
`Effect.flip` checks on the same messages

## ref

- closes: CLI-2742
This adds postgres 17.11.0.006-orioledb-r0 as a new release line.


https://github.com/supabase/slim-services/releases/tag/postgres-17.11.0.006-orioledb-r0

Planned after supabase/slim-services published 15.19.0.006-r0. This
branch is rewritten from develop whenever a newer relevant release
arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps postgres from 15.19.0.005-r0 to 15.19.0.006-r0.


https://github.com/supabase/slim-services/releases/tag/postgres-15.19.0.006-r0

Planned after supabase/slim-services published 15.19.0.006-r0. This
branch is rewritten from develop whenever a newer relevant release
arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps postgres from 17.11.0.005-r0 to 17.11.0.006-r0.


https://github.com/supabase/slim-services/releases/tag/postgres-17.11.0.006-r0

Planned after supabase/slim-services published 15.19.0.006-r0. This
branch is rewritten from develop whenever a newer relevant release
arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…2749) (#7216)

## TL;DR

stack `rest` now comes up even when a `db_pre_request` hook rejects
requests without an app header.

## whats missing?

the stack never started PostgREST's admin server, so it checked
readiness with a plain `GET /`.
that request goes through the project's `db_pre_request` hook, which
answers 403 to anything without the header, so rest never looked ready.

eager start timed out, and lazy start gave the first API request a 502
after a min

## now fixed by:

rest now runs the admin server and checks its `/ready`
same as legacy start, so the hook never sees the probe. recipes pick the
probed port with a new optional `healthPort`, which defaults to `http`

<details>
<summary>visual rep:</summary>

```mermaid
flowchart TB
  subgraph before
    direction LR
    A1["readiness probe"] -->|"GET /"| B1["data port"]
    B1 --> C1["db_pre_request hook"]
    C1 -->|"403"| D1["never ready (eager timeout, lazy 502)"]
  end
  subgraph after
    direction LR
    A2["readiness probe"] -->|"GET /ready"| B2["admin server"]
    B2 -->|"200 once the schema cache loads"| D2["ready"]
  end
  before ~~~ after
```

</details>

## ref:

- closes: #7215
This PR was automatically created to sync API types from the
infrastructure repository.

Changes were detected after refreshing `apps/cli-go/api/v1-openapi.yaml`
from the latest spec from infrastructure, and `apps/cli-go/pkg/api` is
regenerated from it. The snapshot diff is the upstream API change and is
worth reading: it is marked `linguist-generated`, so GitHub collapses it
behind a "Load diff" click rather than hiding it.

Co-authored-by: supabase-cli-releaser[bot] <246109035+supabase-cli-releaser[bot]@users.noreply.github.com>
This bumps realtime from v2.145.3-r0 to v2.145.4-r0.


https://github.com/supabase/slim-services/releases/tag/realtime-v2.145.4-r0

Planned after supabase/slim-services published v2.145.4-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps edge-runtime from v1.77.4-r0 to v1.77.5-r0.


https://github.com/supabase/slim-services/releases/tag/edge-runtime-v1.77.5-r0

Planned after supabase/slim-services published v1.77.5-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps realtime from v2.145.4-r0 to v2.145.5-r0.


https://github.com/supabase/slim-services/releases/tag/realtime-v2.145.5-r0

Planned after supabase/slim-services published v2.145.5-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@supabase-cli-releaser
supabase-cli-releaser Bot requested review from a team and supabase-oss as code owners October 20, 2026 02:22
@supabase-cli-releaser supabase-cli-releaser Bot added the do not merge Approve to apply; do not merge. label Oct 20, 2026
Comment thread apps/cli/src/commands/link/link.integration.test.ts Fixed
This bumps postgrest from v16.4-r0 to v16.5-r0.


https://github.com/supabase/slim-services/releases/tag/postgrest-v16.5-r0

Planned after supabase/slim-services published v16.5-r0. This branch is
rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This bumps edge-runtime from v1.77.5-r0 to v1.77.6-r0.


https://github.com/supabase/slim-services/releases/tag/edge-runtime-v1.77.6-r0

Planned after supabase/slim-services published v1.77.6-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
jgoux and others added 12 commits October 20, 2026 10:14
## Summary

Stack mode can now:

- run any service version that slim-services has published;
- match the hosted project's versions after `supabase link`;
- reproduce the whole local stack from one committed file,
`supabase/supabase.lock`.

Before this change, every service version was compiled into the CLI. You
couldn't try a hotfix, match production, or keep a team on the same
builds without everyone running the same CLI release.

Legacy (docker-compose) mode does not change, except for one fix: a
`SUPABASE_PROJECT_ID` naming another project than the linked one now
gets its pooler URL from the Management API, as `--project-ref` already
did, instead of reusing the linked project's saved
`supabase/.temp/pooler-url`. Everything below applies only to stack mode
(`SUPABASE_EXPERIMENTAL_STACK=1` or `[experimental] stack = true`).

> Version numbers and project refs in the examples are illustrative.

---

## The mental model: it works like a package manager

If you know `package.json` + `pnpm-lock.yaml`, or `Cargo.toml` +
`Cargo.lock`, you already know this.

| Package manager | Supabase stack mode | Committed? | Purpose |
| --- | --- | --- | --- |
| `package.json` | `[local.services]` in `supabase/config.toml` | ✅ |
What you **ask for**: `linked`, `bundled`, `v2.197.0`, `v2.197.0-r1` |
| `pnpm-lock.yaml` | `supabase/supabase.lock` | ✅ | What you **got**:
the exact build of every service, pinned by digest and checksum |
| — | `supabase/.link.json` | ❌ (gitignored) | Which hosted project or
branch *this checkout* targets |
| `pnpm install` | `supabase start` / `supabase stack install` | |
Resolves what changed, then runs or downloads the locked builds |
| `pnpm update` | `supabase stack update [svc…]` | | Re-resolves on
purpose and rewrites the lock |
| `pnpm outdated` | `supabase stack outdated` | | Read-only report of
what an update would change |
| `pnpm install --frozen-lockfile` | `supabase start --frozen` /
`supabase stack install --frozen` (implied when `CI` is set and a lock
exists) | | Fails instead of changing the lock |

```mermaid
flowchart LR
  subgraph git["Committed to git"]
    CFG["supabase/config.toml<br/>[local.services]<br/><i>what you ask for</i>"]
    LOCK["supabase/supabase.lock<br/><i>what you got</i>"]
  end
  subgraph checkout["This checkout only"]
    LINK["supabase/.link.json<br/><i>target hosted project</i>"]
  end
  HOSTED[("Hosted project")]
  RELEASES[("slim-services releases")]

  CFG -->|specifiers| START["supabase start<br/>stack install / stack update"]
  RELEASES -->|published builds + checksums| START
  START -->|writes when stale| LOCK
  HOSTED -->|versions it runs| LINKCMD["supabase link"]
  LINKCMD --> LINK
  LINKCMD -->|records observations,<br/>resolves 'linked'| LOCK
  LOCK -->|exact pins| STACK[("Local stack")]
```

---

## How a version is asked for

```toml
[local.services]
default = { version = "linked" }  # the default: follow the linked hosted project

[local.services.auth]
version = "v2.197.0"              # an upstream version: its highest published revision

[local.services.realtime]
version = "v2.144.2-r1"           # exactly revision 1 of that upstream version
```

`bundled` means "the version this CLI ships with". `[db]
orioledb_version` is shorthand for the database specifier, and a
database version must match `db.major_version`.

```mermaid
flowchart TD
  SPEC["specifier of a service"] --> KIND{"which kind?"}
  KIND -->|"linked (default)"| LINKED{"is the project linked,<br/>and does it report a version?"}
  LINKED -->|never linked| B1["bundled pin<br/>unlinked"]
  LINKED -->|service not reported| B2["bundled pin<br/>unobserved"]
  LINKED -->|reported| PUB{"published by<br/>slim-services?"}
  PUB -->|no| B3["bundled pin<br/>unpublished"]
  PUB -->|yes| R1["the hosted release<br/>resolved"]
  KIND -->|bundled| R2["this CLI's bundled pin"]
  KIND -->|"v2.197.0"| R3["highest published revision<br/>of v2.197.0"]
  KIND -->|"v2.197.0-r1"| R4["exactly v2.197.0-r1"]
```

The fallback reasons (`unlinked`, `unobserved`, `unpublished`) are
recorded in the lock, so nobody has to guess why a service isn't running
the hosted version.

## What the lock contains

One entry per service (database, rest, auth, realtime, storage,
imgproxy, functions, studio, pgmeta, mail, analytics, pooler). Keys are
sorted and the document is written atomically, so diffs and merge
conflicts stay readable.

```jsonc
{
  "$schema": "https://supabase.com/docs/cli/supabase-lock/v1.schema.json",
  "lockfileVersion": 1,
  "linked": {                                   // the remote the observations came from
    "project": "abcdefghijklmnopqrst",
    "branch": { "ref": "abcdefghijklmnopqrst", "isDefault": true },
    "observedAt": "2026-10-16T09:12:44.000Z"
  },
  "services": {
    "auth": {
      "specifier": "linked",                    // what config.toml asked for
      "linked": { "raw": "v2.196.1", "version": "v2.196.1" },  // what the hosted project reported
      "resolution": "resolved",                 // or unlinked / unobserved / unpublished
      "resolved": {                             // the exact build that runs
        "upstreamVersion": "v2.196.1",
        "revision": 0,
        "upstreamImage": "supabase/gotrue:v2.196.1",
        "image": "ghcr.io/supabase/cli/gotrue:v2.196.1-r0@sha256:…",
        "natives": { "darwin-arm64": { "archive": "…", "manifest": "…" }, "…": {} }
      }
    },
    "database": { "majorVersion": 17, "line": "stock", "…": "…" }
    // …one entry per service
  }
}
```

---

## What `supabase start` does with the lock

```mermaid
flowchart TD
  S["supabase start"] --> FRESH{"lock exists, and every entry<br/>still matches config.toml?"}
  FRESH -->|yes| RUN["run the locked builds<br/>(no network, no Management API)"]
  FRESH -->|"no (missing or stale)"| FROZEN{"frozen?<br/>--frozen, or CI with a lock"}
  FROZEN -->|yes| FAIL["fail: lock missing or stale<br/>→ run supabase start locally and commit it"]
  FROZEN -->|no| LEASE["take the project lease<br/>(serializes concurrent starts)"]
  LEASE --> RESOLVE["re-resolve only the services<br/>whose specifier changed"]
  RESOLVE --> WRITE["write supabase.lock atomically"]
  WRITE --> RUN
```

A start never calls the Management API and needs no login. It only warns
when:

- `.link.json` targets another project or branch than the lock observed
(run `supabase stack update`);
- this CLI bundles a newer revision of a locked version.

---

## Typical workflows

### 1. New project, never linked

```console
$ supabase start
Resolved service versions:
  database  17.11.0.004-r0  (specifier linked; unlinked)
  auth      v2.197.0-r0  (specifier linked; unlinked)
  realtime  v2.144.2-r0  (specifier linked; unlinked)
  …
Created supabase/supabase.lock.
```

Nothing is linked yet, so `linked` falls back to the bundled builds and
says so. Commit `supabase/supabase.lock`, and every teammate and CI job
now runs exactly these builds, whatever CLI version they have.

### 2. Match production: `supabase link`

```console
$ supabase link --project-ref abcdefghijklmnopqrst
Hosted versions:
  service   hosted       locked          resolution
  database  17.6.1.011   17.6.1.011-r0   resolved
  auth      v2.196.1     v2.196.1-r0     resolved
  realtime  v2.143.0     v2.143.0-r2     resolved
  studio    -            2026.10.12-…    unobserved
  …
Updated supabase/supabase.lock: now observing abcdefghijklmnopqrst (5 version changes). Review and commit it.
Finished supabase link.
```

`link` probes the hosted project, or the branch, and records what it
runs. Every `linked` service moves to that release. `link` writes
`.link.json` and the lock together, under the same project lease as
`start`. The next `supabase start` runs the hosted versions.

Linking a preview branch works the same way. The lock records which
project and branch it observed, so `start` warns if the checkout's link
later points elsewhere.

### 3. Try one specific version, or a hotfix revision

```toml
[local.services.auth]
version = "v2.198.0"
```

```console
$ supabase start
Resolved service versions:
  auth      v2.196.1-r0 → v2.198.0-r0  (specifier v2.198.0 ≠ locked linked)
Updated supabase/supabase.lock.
```

Only the changed entry is re-resolved, and every other pin stays where
it was. Remove the override to go back to `linked`. Pinning
`v2.198.0-r1` selects exactly that revision.

### 4. A teammate clones the repo, and CI

```console
$ git clone … && cd my-app
$ supabase start          # the lock is fresh: runs the committed builds, no resolution
```

In CI (`CI` set and a lock present), `start`, `stack install` and `link`
are frozen: a stale or missing lock fails instead of being rewritten.

```console
$ CI=1 supabase start
supabase/supabase.lock is stale for: auth (specifier v2.198.0 ≠ locked linked); run supabase start locally and commit the updated lock
```

`supabase stack install --frozen --runtime native` downloads everything
the lock pins without creating a stack, which makes it handy as a cached
CI warm-up step.

### 5. Upgrading the CLI

Upgrading never changes locked versions. If the new CLI bundles a newer
revision of a version you locked, `start` mentions it:

```console
$ supabase start
This CLI bundles newer revisions of locked versions: realtime v2.144.2-r0 → v2.144.2-r1. Run supabase stack outdated to review, supabase stack update to adopt them.
```

```console
$ supabase stack outdated
   SERVICE  | SPECIFIER | HOSTED   | LOCKED      | UPDATE      | BUNDLED     | STATUS           
  ----------|-----------|----------|-------------|-------------|-------------|------------------
   auth     | linked    | v2.196.1 | v2.196.1-r0 | -           | v2.197.0-r0 | up to date       
   realtime | bundled   | -        | v2.144.2-r0 | v2.144.2-r1 | v2.144.2-r1 | update available 
   …
Run supabase stack update to adopt the updates.

$ supabase stack update realtime     # or no argument for every service
```

`stack outdated` is read-only. `stack update` re-resolves the named
services even when the lock is fresh:

- an upstream version moves to its newest revision;
- `bundled` moves to the version this CLI bundles;
- `linked` probes the hosted project again;
- an exact `-rN` pin stays.

### 6. Unlinking

`supabase unlink` removes `.link.json` and the stored credential under
the project lease. The lock is not touched: it stays a valid, reviewed
lock, and its `linked` entry still shows where the observations came
from.

---

## Your local data is protected when versions change

Changing a version must never silently corrupt a local database.

| Change | What happens |
| --- | --- |
| A new revision of the same database build | Accepted |
| Any other database build (another upstream version or Postgres major,
or switching between stock and OrioleDB) with initialized data |
Refused. The advice offers pinning the recorded build under
`[local.services.database]` to keep the data (setting `[db]
major_version` back or removing `[db] orioledb_version` where they would
conflict with that pin), or `supabase stack destroy --stack-id …` (this
deletes the local data) |
| The same change on a database that never initialized | Adopted |
| Moving auth, storage or realtime **back** to a version older than one
that already migrated this database | Refused: a downgrade doesn't
restore the old schema |

`link` and `stack update` still move `linked` pins, but they warn when a
saved stack's data would refuse the new pins.

The database records the build that initialized it. Each schema-owning
service records the newest version that migrated it. Snapshots, restores
and `db reset` carry and respect both records.

---

## Stack mode no longer uses `supabase/.temp`

| Before (`supabase/.temp`) | Now in stack mode |
| --- | --- |
| Project ref, pooler URL, branch | `supabase/.link.json` (gitignored) |
| Postgres and edge-runtime versions | `supabase/supabase.lock` |
| Storage uploads | `$SUPABASE_HOME/stacks/<id>/uploads`, removed by
`stack destroy` |
| Scratch files (pgdelta debug bundles, Functions bundling) |
`$SUPABASE_HOME/tmp` |
| Release-check cache | `$SUPABASE_HOME/cli-latest` |

Delegated Go commands skip `.temp` too. Remote commands resolve the
hosted target once per command, in this order:

1. the `--project-ref` flag;
2. `SUPABASE_PROJECT_ID`;
3. the link;
4. the interactive picker.

Three commands change with this beyond the `.temp` move:

- `gen keys` resolves its project this way, picker included, and passes
it to the delegated command;
- the upgrade notice picks its release-check cache by the project's
mode, read from the working directory it was handed;
- `db pull` builds its shadow database from the locked pins, falling
back to the bundled database pin of the major a `[remotes]` block
selects.

`supabase stack install` replaces the experimental `supabase stack
prepare`.

---

## Reading this PR

There is one commit per chunk, in dependency order; each builds on the
previous one.

| Commit | What it adds |
| --- | --- |
| `docs(stack)` | ADRs 0026 and 0017: the lock-based version model |
| `feat(config)` | `[local.services]` specifiers in the config schema |
| `feat(stack)` resolve pins | Resolving slim-services releases with
checksum, manifest and provenance verification |
| `refactor(cli)` `.temp` layout | One owner for every legacy `.temp`
path |
| `feat(stack)` explicit pin | Every service creation carries the exact
pin it runs |
| `feat(cli)` `.link.json` | The hosted link file in stack mode |
| `feat(stack)` lock format | The `supabase.lock` schema, deterministic
write, typed read errors |
| `feat(stack)` test stacks | `@supabase/stack` test stacks run the
project lock |
| `feat(stack)` database build guard | Initialized data is tied to its
build |
| `feat(cli)` hosted target | One hosted-target resolution per command |
| `feat(stack)` schema writers | Refuses older schema writers on
migrated databases |
| `feat(cli)` lock at start | Resolve and lock at `start`; `--frozen`
and CI behavior |
| `feat(cli)` install | `stack install`, `stack prepare` removed |
| `feat(cli)` update/outdated | `stack update` and `stack outdated` |
| `feat(cli)` linked versions | `link` observes hosted versions and
resolves `linked` |
| `feat(cli)` no `.temp` | Stack mode leaves `supabase/.temp` alone |
| `feat(cli)` Go off `.temp` | Delegated Go commands do the same |
| `test(cli)` | Explicit timeouts on owner-spawning destroy tests |

Review fixes and merges from `develop` follow these commits.

User-facing reference:
[`apps/cli/docs/stack-commands.md`](https://github.com/supabase/cli/blob/juliengoux/cli-2688-stack-mode-lockable-service-versions-with-hosted-parity-via/apps/cli/docs/stack-commands.md#service-versions)
and the [`start` side
effects](https://github.com/supabase/cli/blob/juliengoux/cli-2688-stack-mode-lockable-service-versions-with-hosted-parity-via/apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md#service-versions-and-the-lock).

---------

Co-authored-by: Colum Ferry <cferry09@gmail.com>
## Summary

The `link` and `bootstrap` integration tests picked out Management API
requests with `url.startsWith("https://api.supabase.com")`, which CodeQL
reports as incomplete URL substring sanitization (code scanning alerts
#59 and #60). They now parse each request URL and compare its origin.
The alerts also fail the CodeQL check on unrelated PRs whenever a diff
shifts those lines.
## Summary

v3 (CLI-2695, landing on `next`) makes the local stack the default
target for every command that can run against either the local stack or
a hosted project, with one `--remote` flag to reach a project. Today
nothing tells you that's coming. This adds a warning on develop, without
changing any default:

```
Warning: supabase db push will target the local database by default in the next major version. Pass --linked to keep targeting the linked project.
```

- Printed on stderr, once, when one of these commands runs with no
target selector and goes to the linked project: `db push`, `db pull`,
`db dump`, `migration fetch`, `migration list`, `migration repair`,
`inspect db *` (including the deprecated aliases, named as typed),
`inspect report`, `gen types`, `storage ls/cp/mv/rm`. Storage says "the
local Storage API".
- Any of `--linked`, `--local`, `--db-url`, `--project-ref` (or `gen
types --project-id`) silences it, in any form including
`--linked=false`. `SUPABASE_PROJECT_ID` doesn't, since it won't select
the project in v3.
- `gen types` warns only once a project ref has resolved, so an unlinked
run still prints just the existing "Must specify one of" error.
- stdout, exit codes and the target itself are unchanged in every output
format.

The opt-in `target` config key and `SUPABASE_TARGET` this PR previously
added are gone; the v3 design drops them in favour of `--remote`.

## Linked issue

Part of CLI-2695 (the non-breaking half for the current major).

## Checklist

- [x] The PR title follows Conventional Commits.
- [x] Tests added or updated for the change.
- [x] From the repository root, `pnpm check:all` passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…nts (#7211)

## TL;DR

Two `stack start` failure fingerprints each covered many unrelated
causes, so telemetry could not say what failed. Each start configuration
check now has its own fingerprint, and an unclassified stack failure
names the stack operation that failed.

## What changes

- `tag:ExperimentalStackStartError:invalid_config` splits by check:
`project_config`, `service_config`, `functions_env`,
`saved_stack_changes`, `credentials_conflict`, `missing_database`,
`missing_credentials`.
- Failing to create the Storage uploads, Functions, or Studio snippets
directory is classified by its file-system cause (`filesystem`
permission, else `filesystem_io`) instead of as a user config error.
- A stack failure with no known kind or reason reports
`unclassified_<operation>` for the operations `stack start` runs (for
example `unclassified_start_composition`, `unclassified_start_service`,
`unclassified_ready_service`), covering both the client and the stack
owner side. Other operations keep `unclassified`. This applies to every
stack command wrapper and `functions serve`.

Error messages and output are unchanged. A wrapper that keeps its own
fallback for an unclassified stack failure still does.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Stacked on #7211.

## TL;DR

Every experimental stack command reported every target-resolution
failure under one `invalid_config` fingerprint. That covers reading
saved stack state, decoding a saved stack, and resolving the project
root, so telemetry could not tell these causes apart. Each failed step
now has its own fingerprint in every stack command.

## Before

```mermaid
flowchart LR
  A["stack discovery fails"] --> X["&lt;command&gt;:invalid_config"]
  B["stack lookup fails"] --> X
  C["prefix match unreadable"] --> X
  D["project root unresolvable"] --> X
```

## After

```mermaid
flowchart LR
  A["stack discovery fails"] --> A2["&lt;command&gt;:stack_discovery"]
  B["stack lookup fails"] --> B2["&lt;command&gt;:stack_lookup"]
  C["prefix match unreadable"] --> C2["&lt;command&gt;:unreadable_stack"]
  D["project root unresolvable"] --> D2["&lt;command&gt;:project_root"]
```

## Why

Review on #7211 pointed out that the only remaining `invalid_config`
path in `stack start` is target resolution. That path is shared by all
stack commands, so it was left for a follow-up.

## What changed

- `StackTargetError` carries a closed `targetIssue` that the resolver
sets where each failure occurs.
- `mapTargetError` carries it into the start, stop, restart, status,
destroy, prepare, and logs errors. They report `stack_discovery`,
`stack_lookup`, `unreadable_stack`, or `project_root` instead of
`invalid_config`. In `stack start`, a configuration check's own
fingerprint still takes priority.
- prepare, restart, logs, and stop now use the shared `mapTargetError`
instead of local copies that would drop the new field.
- An invalid-config error without a `targetIssue` still reports
`invalid_config`. Error tags, messages, and output are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## TL;DR

`supabase db push --linked` stops silently dropping `PGSSLMODE` and
`PGSSLROOTCERT` when the direct host answers

## whats introduced?

`--linked` commands that connect from the CLI itself (`db push`,
`migration` commands, `inspect db`, …
now read `PGSSLMODE`, `PGSSLROOTCERT` and `PGSSLCERT`/`PGSSLKEY` from
the shell on the direct host, the way the pooler fallback already did.

ps: the `pg_dump` steps in `db dump`, `db pull` and `migration squash`
still don't forward them.

`PGSERVICE` and `PGCONNECT_TIMEOUT` apply too, and a missing
`PGSSLROOTCERT` file now fails instead of being ignored.

host, role and database stay pinned. an invalid mode fails naming the
vars on either route, and `sslmode=disable` now dials plaintext without
reading cert files.

<details>
<summary>visual rep</summary>

| `supabase db push --linked` | before | now |
| ------------------------------------ | -------------------- |
---------------------------- |
| direct host | `PGSSL*` ignored | read from the shell |
| pooler fallback | read from the shell | unchanged |
| invalid `PGSSLMODE` | ignored / IPv6 error | fails naming the vars |

</details>

## why this?

`--db-url` and the pooler fallback already read these vars.

the linked direct host was skipped by mistake during the port, so it now
reads them too

## ref:

- closes: CLI-2753
- broken/missed in: #5715
This bumps edge-runtime from v1.77.6-r0 to v1.77.8-r0.


https://github.com/supabase/slim-services/releases/tag/edge-runtime-v1.77.8-r0

Planned after supabase/slim-services published v1.77.8-r0. This branch
is rewritten from develop whenever a newer relevant release arrives.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…7262)

## TL;DR

brings `io.ts` and its tests under the effect lint

## whats introduced?

- `packages/config/src/io.*` joins the allow list
- `writeFileAtomic` reads the temp suffix time from the effect `Clock`,
the same value `Date.now()` gave
- `io.unit.test.ts` moves to effect: `FileSystem` and `Path`, scoped
temp dirs, schema json codecs, and `vi.stubEnv` inside a scoped acquire
and release instead of writing `process.env`

## ref

- related: CLI-2756
…I-2756) (#7263)

## TL;DR

brings the rest of `packages/config/src` under the effect lint
## whats introduced?

- `packages/config/src/**` joins the allow list, and the `schema-number`
modules stay out until the schema fix on top
- `project.ts` yields its parse error directly instead of through
`Effect.fail`
- the `./io` methods are plain promise returning functions behind
`Effect.suspend` instead of `async`, so they resolve and reject exactly
as before and never throw synchronously, only `isAsyncFunction` flips
- the src tests move to effect, and the entrypoint purity and import
contract walkers run on `FileSystem`, skipping entries that vanish mid
scan

## ref

- related: CLI-2756
#7265)

## TL;DR

brings the `packages/config` build and release scripts under the effect
lint

## whats introduced?

- the allow list widens to `packages/config/**`
- `build.ts`, `release-plan.ts` and the semantic release path filter run
as effect programs with the same output, exit codes, release outputs and
temp dir cleanup
- a malformed `package.json` now reports a schema decode message instead
of the native json parse one, same exit code

## ref

- closes: CLI-2757
#7267)

## TL;DR

a broken config now says what is wrong and where, and `inf` or `nan`
numbers no longer load

## whats hurting the user?

`supabase start`, `status`, `stop` and `functions serve` print a bare
`CliConfigParseError` for any broken config.

`config push`, `gen types`, `seed buckets` and the rest print the raw
parser output, which quotes nearby lines of the file, secrets included.

`max_rows = inf` or a `nan` rate limit loads quietly and reaches the
containers as `Infinity`

## now fixed by?

every command prints the field path or the line and column, never the
file contents.

number fields only take finite values, so the json schema post
processing that hid the `inf` and `nan` variants goes away.

> [!NOTE]
> a config with `inf` or `nan` in a number field used to load and now
fails, so set a real number instead.

## ref:

- closes: CLI-2756
- closes: CLI-2755
Adds a PERMISSIONS section to `--help` for every command, listing the
personal access token permissions it needs.

Every command already declares the Management API operations it may
call. The help section resolves those declarations against the
permissions the API spec lists for each operation, so users can see
which token to create before running a command.

- Permissions come from a generated `operationId` to `x-fga-permissions`
/ `x-oauth-scope` table in `@supabase/api/permissions`.
- Conditions are worded against the command's own flags: a flag the
command lacks is never named, equivalent cases merge, and a permission
already shown under a broader condition is not repeated. Requirements
that depend on something other than a flag show as "May be required",
without a reason.
- `--output-format json` help carries the same section under a top-level
`permissions` field, with each group's flag conditions as data. The raw
permission declaration is no longer serialized into `doc.annotations`.
- Each permission scheme is resolved and rendered on its own, so OAuth
scopes can be added later by registering one more scheme.

Output sample:
```
$ supabase projects api-keys --help
DESCRIPTION
  List all API keys for a Supabase project.
... rest of the content ...
PERMISSIONS
  Personal access token (FGA):
    Required with --reveal:
      api_gateway_keys_read + api_gateway_keys_secret_read
    Required without --reveal:
      api_gateway_keys_read
    May be required (without --project-ref):
      projects_read
    Optional (command still runs without these):
      project_admin_read
```

```
$ supabase projects api-keys --help --output-format json
...
 "permissions": {
        "status": "mapped",
        "schemes": [
            {
                "scheme": "fga",
                "title": "Personal access token (FGA)",
                "groups": [
                    {
                        "heading": "Required with --reveal",
                        "need": "required",
                        "when": [
                            {
                                "all": [
                                    {
                                        "flag": "reveal",
                                        "values": [
                                            "true"
                                        ]
                                    }
                                ]
                            }
                        ],
                        "rows": [
                            {
                                "anyOf": [
                                    [
                                        "api_gateway_keys_read",
                                        "api_gateway_keys_secret_read"
                                    ]
                                ]
                            }
                        ]
                    },
                    {
                        "heading": "Required without --reveal",
                        "need": "required",
                        "when": [
                            {
                                "all": [
                                    {
                                        "flag": "reveal",
                                        "values": [
                                            "false"
                                        ]
                                    }
                                ]
                            }
                        ],
                        "rows": [
                            {
                                "anyOf": [
                                    [
                                        "api_gateway_keys_read"
                                    ]
                                ]
                            }
                        ]
                    },
                    {
                        "heading": "May be required (without --project-ref)",
                        "need": "maybe",
                        "when": [
                            {
                                "all": [
                                    {
                                        "flag": "project-ref",
                                        "values": [
                                            "unset",
                                            "empty"
                                        ]
                                    }
                                ]
                            }
                        ],
                        "rows": [
                            {
                                "anyOf": [
                                    [
                                        "projects_read"
                                    ]
                                ]
                            }
                        ]
                    },
                    {
                        "heading": "Optional (command still runs without these)",
                        "need": "optional",
                        "rows": [
                            {
                                "anyOf": [
                                    [
                                        "project_admin_read"
                                    ]
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
...
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active (outdated) deployment
auto-approve — f21563ca Deployed Oct 20, 2026 by supabase-cli-releaser[bot] via Approve the reviewed sync #2721
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Approve to apply; do not merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants