Repository navigation
Conversation
bfde02c to
a334008
Compare
A top-level [local] table carries per-service version specifiers for the local stack (linked, bundled, <U>, <U>-rN). Hosted commands never read it, and the Go config accepts and ignores it.
resolvePin probes slim-services GitHub releases for an upstream version's highest (or a given) revision, verifies SHA256SUMS, per-target manifests and mirror provenance, and reads the GHCR digest. Release parsing is shared with the catalog sync scripts. Internal URL overrides also apply to the owner's native downloads.
One module defines every .temp path, a LegacyProjectState service owns the link and cache reads and writes, and projectStateMode is the single env-then-config rule for stack mode.
Every stack service creation now carries a required `pin` (upstream version, revision, image, upstream image, native archives). The stack owner never resolves versions; callers choose pins per command through `defaultArtifactSelection()` or the stack artifact selection. - `resolveArtifact` and client commands take the pin instead of a catalog version or major. - A database release-line change (stock major or exact OrioleDB build) is incompatible; any other pin change restarts the service. - A configuration-only restart keeps the saved pin; the owner fills it in. - Readiness markers, snapshots, and the shadow cache key on the full pin.
In stack project-state mode, the hosted project and branch this checkout targets live in a gitignored `supabase/.link.json` instead of `supabase/.temp`. Legacy mode is unchanged. - `link`, `bootstrap`, `unlink`, and `projects delete` write or remove the document; `link` adds the `.gitignore` entries and the pooler URL keeps the existing transform. - Remote commands read the project ref, parent, and pooler URL from it. - An invalid `SUPABASE_EXPERIMENTAL_STACK` fails only commands that depend on the mode. - Bootstrap records the project's organization and identifies its telemetry groups.
The stack owns the lock document: an Effect Schema for lockfileVersion 1, a sorted-key deterministic write published atomically, a read that reports a newer lockfileVersion as "upgrade the CLI" and a malformed lock as invalid, and the conversion from a lock to an artifact selection.
A test stack given a caller-owned project root runs the pins in its supabase/supabase.lock, and the bundled pins when there is none. A lock that cannot be read fails creation with its typed lock error, before any stack exists.
The database records its line, major and build before initdb, and every reuse path refuses initialized data under any other build; a new slim-services revision of the same build is accepted. Snapshots carry the same identity. db start rebinds a stopped database to the selected creation, keeping its id, data and the other composition members (retainMembers).
RemoteTarget resolves the hosted project from the flag, SUPABASE_PROJECT_ID, the link (supabase/.link.json in stack mode, .temp in legacy mode), or the interactive picker, and carries the parent ref, branch name, and pooler URL only when the resolved ref is the linked one. Commands load it once and share it; Go-delegated commands receive the resolved ref in stack mode, and the stack-mode telemetry cache lives under SUPABASE_HOME.
The database instance records the newest upstream version of each schema-owning service that migrated it in schema-writers.json. Service launches and the auth, storage, and realtime initialization commands raise the record before migrating and refuse an older version with the stack destroy remedy. Snapshots carry the record, and restores order the record around the data so an interrupted restore never admits an older writer.
Stack-mode start resolves [local.services] specifiers into supabase/supabase.lock under a short lease, reuses a fresh lock as-is, and fails with --frozen (or in CI with an existing lock) when the lock is missing or stale. Reset, shadow, serve, services and status read the lock and fall back to bundled pins. The lock format is published as a JSON schema.
`supabase stack install` resolves the project lock like start (honouring --frozen and CI), then downloads everything the lock pins for the chosen runtime without creating a stack. packages/stack gains a stackless artifact-prepare export, and the experimental `stack prepare` command is removed.
`supabase stack update [svc…]` re-resolves the named services (or all) under the project lease and rewrites the lock, reporting unnamed stale entries instead of touching them. `supabase stack outdated` is a read-only report of newer revisions, newer bundled pins and stale entries.
In stack mode, `supabase link` probes the hosted project or branch, records the observations in supabase/supabase.lock and resolves `linked` specifiers from them, falling back to bundled pins when a service is not reported or a probe fails. The lock and .link.json are written together under the project lease; `link --frozen` (implied in CI with a lock) leaves the lock untouched. `stack update` re-probes linked entries, `stack outdated` shows hosted observations, and start warns when the lock observed another remote or when this CLI bundles a newer revision of a locked version.
In stack mode no command reads or writes supabase/.temp. Storage uploads live under $SUPABASE_HOME/stacks/<id>/uploads and are removed by stack destroy, scratch files go to $SUPABASE_HOME/tmp, the upgrade notice caches in $SUPABASE_HOME/cli-latest, and Postgres and edge-runtime versions come from supabase/supabase.lock. Legacy mode is unchanged.
In stack mode the CLI sets SUPABASE_INTERNAL_STACK_STATE for the Go child, which then skips every supabase/.temp read and write (config pins and pooler URL, project ref, linked-project cache, update notifier). The linked project identity from .link.json reaches Go telemetry through SUPABASE_INTERNAL_LINKED_PROJECT. Go db remote accepts --project-ref, which db remote changes now passes instead of SUPABASE_PROJECT_ID.
a334008 to
a7927b3
Compare
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Verified all seven findings against the checked-out code and trusted conventions. All seven are confirmed, with no duplicates. The highest risks are arbitrary-registry container execution and premature migration-lease release. Verification was static; tests were not run.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🔴 CRITICAL | packages/stack/src/Lock.ts:31 |
security |
claude | The lock accepts service images from arbitrary registries. An edited lock can therefore make container-based stack commands or slim Functions tooling execute attacker-published images with the service's credentials and mounts. |
| 🟠 MAJOR | packages/stack/src/Owner.ts:526 |
concurrency |
codex | A failed or interrupted readiness check releases the schema-writer lease before the service process has stopped, allowing another initializer to acquire it while the original process may still be migrating. |
| 🟡 MINOR | apps/cli/src/command-internal/stack-lock.ts:418 |
correctness |
claude | Resolving a stale linked entry during a release-host outage writes a bundled fallback as unobserved. Subsequent starts consider that entry fresh and never retry automatically, even though its hosted version was observed. |
| 🟡 MINOR | apps/cli/src/command-internal/stack-config.ts:983 |
state-management |
codex | Start, install, and update can create or rewrite supabase.lock before remaining configuration translation rejects the command. |
| 🟡 MINOR | packages/config/src/local.ts:5 |
validation |
claude | The config schema accepts upstream versions containing +, but release resolution rejects them as unpublished instead of reporting an invalid version during configuration validation. |
| ⚪ NIT | apps/cli/src/commands/experimental/stack/start/SIDE_EFFECTS.md:270 |
documentation |
codex | The start documentation incorrectly promises that a same-major PostgreSQL upstream build change can restart successfully against initialized data. |
| ⚪ NIT | apps/cli/src/commands/services/SIDE_EFFECTS.md:95 |
documentation |
codex | The services mismatch guidance still describes catalog-only versions and misleadingly says supabase link cannot change them, although displayed stack versions now come from the project lock. |
Stats
Claude findings: 3 · Codex findings: 4 · Confirmed: 7 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: auto · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
- Lock images must be the canonical GHCR reference; mirrors are derived at pull time. - The schema-writer lease is released only after a successful readiness check, or with the launch scope after process cleanup. - Start, install and update publish supabase.lock only after the whole config, including local auth keys, validates. - Upstream versions in [local.services] reject "+". - Correct the stack start reuse rules and the stack-mode services mismatch guidance.
|
/ai-review |
There was a problem hiding this comment.
Superseded by a newer AI review
🤖 AI Review
Verified all eight supplied findings against the surrounding PR code and trusted conventions. All eight hold, with the lease-contention finding narrowed because the API client already applies a request timeout. The main risks are blocked reuse of existing database data, unannounced database-build changes during linking, and incompatible dump-client selection. Verification was static; runtime tests were not run.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | packages/stack/src/internal/database-reuse.ts:54 |
backward-compatibility |
claude | Previously initialized database data without the new identity file is refused after upgrading, even when the requested stock PostgreSQL major matches. The offered recovery destroys local data. |
| 🟠 MAJOR | packages/stack/src/internal/database-reuse.ts:28 |
backward-compatibility |
claude | Linking or updating can change the default linked database pin to another build within the same stock PostgreSQL major, leaving initialized local data unable to start without warning when the pin changes. |
| 🟠 MAJOR | apps/cli/src/command-internal/db-pull-run.ts:492 |
correctness |
codex | An initial migration-mode db pull using migra in stack mode ignores remote PostgreSQL configuration overrides when selecting its dump image, potentially selecting pg_dump older than the target server. |
| 🟡 MINOR | apps/cli/src/command-internal/stack-lock.ts:418 |
correctness |
claude | A transient release-host failure while resolving a stale linked entry can permanently record a bundled fallback as unobserved, although the hosted version was observed. Subsequent ordinary starts do not retry. |
| 🟡 MINOR | apps/cli/src/command-internal/stack-lock.ts:1200 |
concurrency |
claude | Link and update hold the project lease throughout hosted probing and release resolution, which can exceed the 60-second acquisition budget and make concurrent starts fail with lease-busy. |
| 🟡 MINOR | apps/cli/src/command-internal/stack-lock.ts:905 |
correctness |
claude | Bundled-version reporting compares OrioleDB locks against the stock database pin, suppressing warnings about newer bundled revisions of the locked OrioleDB build and displaying a stock bundled version. |
| 🟡 MINOR | apps/cli/src/command-internal/upgrade-notice.ts:483 |
correctness |
codex | After bootstrap creates a stack-enabled template project from a legacy-mode directory, the upgrade hook can create supabase/.temp/cli-latest inside the new stack-mode project. |
| ⚪ NIT | apps/cli/src/command-internal/stack-lock.ts:688 |
error-messages |
claude | Explicit frozen mode reports that the lock is missing when configuration is absent, even if the lock exists. |
Stats
Claude findings: 6 · Codex findings: 2 · Confirmed: 8 · Refuted: 0 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
|
/ai-review |
avallete
left a comment
There was a problem hiding this comment.
Dogfooded this at 16ba0b1 (unlinked bootstrap, explicit pins, frozen/CI, linked staging project): the main flows work. A review pass turned up these significant issues:
- Upgrading leaves existing stacks unusable.
packages/stack/src/services/Recipe.tsmakespinrequired in saved stack state with no migration. State saved by an earlier CLI fails to decode (Missing key at ["pin"]), sostartandstack destroy --stack-idboth fail. Recovery means deleting the stack directory by hand and leaves the Docker volume orphaned. Data created before this PR also has no identity file, sointernal/database-reuse.tsrefuses it, and uploads move out of.temp/stack-uploadswithout being migrated. linkandstack updatecan force a destroy. They move database and schema-owner pins (stack-lock.ts~1247) without checking the existing data, and the nextstartrefuses the change. The documented "start unlinked, then link" flow hits this.docs/stack-commands.md:352says only a major or release-line change triggers it.supabase.lockis written as 0600.Lock.ts:212callspublish()withoutmode, which defaults to0o600. That's wrong for a committed file. Fix: passmode: 0o644, asproject-link.tsalready does for.gitignore.- With no lock,
[local.services]pins are ignored.readSelectionOrBundled(stack-lock.ts:647) falls back to bundled pins for shadow, db reset and functions serve. For example, an orioledb database pin gives a stock shadow fordb diffuntil the firststart. - Link is not atomic. The lock is published (
stack-lock.ts:1251) before.link.jsonis written. A failure in between leaves the lock describing the new remote and the link pointing at the old one. - (Moderate confidence)
stack destroydeletes the Storage uploads bind-mount source. It removesstacks/<id>/uploads(destroy.handler.ts:162), whichRegistry.ts:193warns breaks the file share when the same id restarts.destroy && startis the recovery the errors above recommend.
Smaller items:
- The lease file can end up outside
.gitignore. stack installdownloads disabled services.- An invalid
SUPABASE_EXPERIMENTAL_STACKnow fails legacylink/unlink. - The legacy pooler-URL behavior changed, while the description says legacy mode is unchanged.
There was a problem hiding this comment.
🤖 AI Review
Verified all six findings from both reviews: four confirmed and two refuted. Confirmed issues concern database pin changes and destructive recovery advice, external-service schema-writer bookkeeping, the gen keys picker, and caching-test coverage. Refuted the private-state migration requirement under trusted conventions and the pg-schema regression because stack mode rejects that engine before delegation. The picker finding is confirmed only for gen keys.
Findings
| Severity | Location | Category | Sources | Claim |
|---|---|---|---|---|
| 🟠 MAJOR | apps/cli/src/command-internal/stack-lock.ts:1247 |
data-loss |
claude | Linking an existing local stack can change its database build and prevent subsequent starts, without warning about data incompatibility; the resulting error recommends destructive recovery and omits restoring the previous pin. |
| 🟠 MAJOR | packages/stack/src/Owner.ts:514 |
correctness |
codex | A standalone schema-writing service using an external database incorrectly checks and advances the composition's local database migration record, potentially blocking the local service's existing version. |
| 🟡 MINOR | apps/cli/src/command-internal/go-proxy-target-env.ts:10 |
behavior-regression |
claude | In stack mode, gen keys loses its interactive project picker when no flag, environment value, or link supplies a project ref. |
| 🟡 MINOR | apps/cli/src/config/remote-target.layer.integration.test.ts:247 |
test-quality |
codex | The caching test checks tracing implementation details without verifying that repeated resolution preserves the original target after the link changes. |
Refuted findings (kept for transparency, not posted as review comments)
packages/stack/src/services/Recipe.ts:40(backward-compatibility): Earlier managed-stack state cannot decode after pin becomes required; legacy database identities and Storage upload paths also have no migration.
Refuted: The incompatibility is real, but requiring migration of this private experimental state conflicts with the trusted compatibility scope. trusted/docs/adr/0017-simplified-managed-stack-architecture.md:37-39 identifies the managed document as private, unreleased state, and :323-326 explicitly permits its format to change with the current build. trusted/CLAUDE.md:146-149 permits reshaping unreleased APIs without compatibility scaffolding, while trusted/apps/cli/CLAUDE.md:97-101 excludes experimental paths from the stable compatibility promise. This does not establish a supported cross-version migration regression.apps/cli/src/commands/db/diff/diff.handler.ts:125(behavior-regression): Stack-mode db diff --use-pg-schema --linked reaches Go without forwarding the linked project ref and fails to find the project.
Refuted: The claimed delegation is unreachable in stack mode. diff.handler.ts:185-195 calls stackRejectNativeDockerDiffEngine whenever usePgSchema is present. command-internal/stack-local-database.ts:177-184 rejects every stack backend with the explicit message that only pg-delta is supported. stack-backend.ts:27-32 and :123-131 route db commands through that backend selection. The same rejection exists in the trusted default-branch code.
Stats
Claude findings: 4 · Codex findings: 2 · Confirmed: 4 · Refuted: 2 · Uncertain: 0
Models: claude-opus-5-5 + gpt-6.1-sol · Trigger: manual · Workflow run
This review runs once per PR. A maintainer can request another with a /ai-review comment.
- write supabase.lock as 0644; taking the lease gitignores the lease file, frozen reads skip it - honour [local.services] pins without a lock; unbundled ones fail with "run supabase start" - link writes .link.json before the lock - link and stack update warn when an existing stack's data would refuse the new pins - start refusals offer pinning the recorded version before destroy, with major/line-aware advice - explicit --frozen reads the lock before checking config - outdated compares a locked OrioleDB build against its own bundled pin - db pull and the db diff shadow share one selection; a [remotes] block that makes entries stale falls back to bundled pins only for those entries - the upgrade hook resolves stack mode from the handed-over working directory - gen keys uses the remote target picker - schema-writer leases and records follow the writer's databaseUrl binding - document the legacy pooler-url scoping and the SUPABASE_EXPERIMENTAL_STACK exit code on link/unlink
|
Hey @avallete, thanks for dogfooding this, really useful. @jgoux I've pushed fixes for these onto your branch in 72eab92, hope that's ok! I used the PR description's design as the bar for what to change, so a few of these are push-backs.
Smaller ones:
Happy to revisit any of the push-backs if you disagree! |
…stack-mode-lockable-service-versions-with-hosted-parity-via # Conflicts: # apps/cli/src/commands/gen/types/SIDE_EFFECTS.md
…ng test The test assumed the bundled database pin has a revision above r0, which stops holding whenever the database bumps to a new -r0 build.
Tenant version requests carry the service-role key in the apikey header, which fetch keeps across a cross-origin redirect. A 3xx from the tenant host is now returned, so the probe records it as a failed probe.
avallete
left a comment
There was a problem hiding this comment.
Thanks @Coly010, the fixes in 72eab92 hold up. Approving to unblock; the items below are follow-ups, not blockers.
Verified
- Re-dogfooded at 72eab92 (Docker runtime, one throwaway staging project):
- Lock is 0644.
- The lease entry is added to
.gitignore. --frozenandCI=1leave the project untouched.- With no lock, a bundled
[local.services]pin is served and an unbundled pin fails with "run supabase start to create it". - Link writes
.link.jsonbefore the lock. - Link warns about the conflict, and the next start's refusal offers the pin first. Adding that pin lets start reuse the data, and the row survives.
- Destroy then start: Storage uploads work again. I accept that pushback.
- Pushbacks I agree with: upgrading existing stacks (ADR 0017), install fetching every pinned service, the invalid
SUPABASE_EXPERIMENTAL_STACKerror, and the legacy pooler URL change.
Remaining edge cases in the new paths
packages/stack/src/internal/database-reuse.ts:62- The refusal advice suggests restoring
[db] major_version/orioledb_versionbut ignores an explicit[local.services.database]pin. - Example: stock data, then a switch to an explicit OrioleDB pin. The advice tells the user to restore a
[db]setting that was never changed, which either repeats the refusal or fails validation while the pin is still set.
- The refusal advice suggests restoring
packages/stack/src/internal/data-conflicts.ts:69- An interrupted Docker start can leave
PG_VERSIONand a recorded build but no readiness marker. - The link /
stack updatecheck treats this as fresh and stays silent, but the next start's storage prepare checks the build and refuses it. - The test at
data-conflicts.integration.test.ts:108asserts that silence. - Two independent review passes flagged this; I did not reproduce it live.
- An interrupted Docker start can leave
Cleanup
bindsDatabaseUrl(Owner.ts:744) is always true, and theSupabase.unit.test.tstest exists only to pin it. Both can go.- The data-conflict cases are tested in the stack package, in
link.lock.integration.test.tsand inupdate.integration.test.ts.- The two handler files could each keep about 3 wiring tests (warns, quiet, unreadable state still succeeds).
- The interrupted-start case is also duplicated between
data-conflicts.integration.test.tsanddatabase-reuse.unit.test.ts.
- The comment at
stack-data-conflicts.ts:8says one warning per stack, but the loop warns once per conflict. - Changes the reply doesn't mention: the
gen keysproject picker, the mode lookup inupgrade-notice.ts, and the switch indb-pull-run.tsto remote-aware selection. Worth a line in the PR description. - Smaller dogfood notes:
- With no lock and an unbundled pin,
supabase servicesonly warns and lists the defaults instead of failing. - In the same setup,
functions serveends with "Functions service stopped or became unhealthy" rather than the lock error.
- With no lock and an unbundled pin,
…stack-mode-lockable-service-versions-with-hosted-parity-via Brings the PR's config and telemetry code under develop's effect lint coverage, and pins the new PostgREST readiness test's rest creation. # Conflicts: # apps/cli/src/cli/complete.ts # apps/cli/src/cli/main.ts # apps/cli/src/config/project-ref.layer.integration.test.ts # apps/cli/src/config/project-ref.layer.ts # apps/cli/src/config/remote-target.layer.unit.test.ts # apps/cli/src/telemetry/linked-project-cache.integration.test.ts # apps/cli/src/telemetry/linked-project-cache.layer.ts
The refusal advice now always offers the recorded build as a [local.services.database] pin, adding the [db] major_version and orioledb_version edits that keep that pin valid. link and stack update also judge the identity an interrupted Docker or Podman first start recorded. Drops the always-true bindsDatabaseUrl, trims the duplicated handler data-conflict tests, and corrects which functions serve path reads the lock.
supabase/cli#7195 removed `stack prepare`, so every stack-backend run failed with "Unrecognized flag: --capability". `stack start --runtime <runtime>` with every other capability excluded creates the same database-only stack and saves its runtime for `db start`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Stack mode can now:
supabase link;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_IDnaming another project than the linked one now gets its pooler URL from the Management API, as--project-refalready did, instead of reusing the linked project's savedsupabase/.temp/pooler-url. Everything below applies only to stack mode (SUPABASE_EXPERIMENTAL_STACK=1or[experimental] stack = true).The mental model: it works like a package manager
If you know
package.json+pnpm-lock.yaml, orCargo.toml+Cargo.lock, you already know this.package.json[local.services]insupabase/config.tomllinked,bundled,v2.197.0,v2.197.0-r1pnpm-lock.yamlsupabase/supabase.locksupabase/.link.jsonpnpm installsupabase start/supabase stack installpnpm updatesupabase stack update [svc…]pnpm outdatedsupabase stack outdatedpnpm install --frozen-lockfilesupabase start --frozen/supabase stack install --frozen(implied whenCIis set and a lock exists)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
bundledmeans "the version this CLI ships with".[db] orioledb_versionis shorthand for the database specifier, and a database version must matchdb.major_version.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.
{ "$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 startdoes with the lockflowchart 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 --> RUNA start never calls the Management API and needs no login. It only warns when:
.link.jsontargets another project or branch than the lock observed (runsupabase stack update);Typical workflows
1. New project, never linked
Nothing is linked yet, so
linkedfalls back to the bundled builds and says so. Commitsupabase/supabase.lock, and every teammate and CI job now runs exactly these builds, whatever CLI version they have.2. Match production:
supabase linklinkprobes the hosted project, or the branch, and records what it runs. Everylinkedservice moves to that release.linkwrites.link.jsonand the lock together, under the same project lease asstart. The nextsupabase startruns the hosted versions.Linking a preview branch works the same way. The lock records which project and branch it observed, so
startwarns if the checkout's link later points elsewhere.3. Try one specific version, or a hotfix revision
Only the changed entry is re-resolved, and every other pin stays where it was. Remove the override to go back to
linked. Pinningv2.198.0-r1selects exactly that revision.4. A teammate clones the repo, and CI
In CI (
CIset and a lock present),start,stack installandlinkare frozen: a stale or missing lock fails instead of being rewritten.supabase stack install --frozen --runtime nativedownloads 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,
startmentions it:stack outdatedis read-only.stack updatere-resolves the named services even when the lock is fresh:bundledmoves to the version this CLI bundles;linkedprobes the hosted project again;-rNpin stays.6. Unlinking
supabase unlinkremoves.link.jsonand the stored credential under the project lease. The lock is not touched: it stays a valid, reviewed lock, and itslinkedentry 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.
[local.services.database]to keep the data (setting[db] major_versionback or removing[db] orioledb_versionwhere they would conflict with that pin), orsupabase stack destroy --stack-id …(this deletes the local data)linkandstack updatestill movelinkedpins, 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 resetcarry and respect both records.Stack mode no longer uses
supabase/.tempsupabase/.temp)supabase/.link.json(gitignored)supabase/supabase.lock$SUPABASE_HOME/stacks/<id>/uploads, removed bystack destroy$SUPABASE_HOME/tmp$SUPABASE_HOME/cli-latestDelegated Go commands skip
.temptoo. Remote commands resolve the hosted target once per command, in this order:--project-refflag;SUPABASE_PROJECT_ID;Three commands change with this beyond the
.tempmove:gen keysresolves its project this way, picker included, and passes it to the delegated command;db pullbuilds its shadow database from the locked pins, falling back to the bundled database pin of the major a[remotes]block selects.supabase stack installreplaces the experimentalsupabase stack prepare.Reading this PR
There is one commit per chunk, in dependency order; each builds on the previous one.
docs(stack)feat(config)[local.services]specifiers in the config schemafeat(stack)resolve pinsrefactor(cli).templayout.temppathfeat(stack)explicit pinfeat(cli).link.jsonfeat(stack)lock formatsupabase.lockschema, deterministic write, typed read errorsfeat(stack)test stacks@supabase/stacktest stacks run the project lockfeat(stack)database build guardfeat(cli)hosted targetfeat(stack)schema writersfeat(cli)lock at startstart;--frozenand CI behaviorfeat(cli)installstack install,stack prepareremovedfeat(cli)update/outdatedstack updateandstack outdatedfeat(cli)linked versionslinkobserves hosted versions and resolveslinkedfeat(cli)no.tempsupabase/.tempalonefeat(cli)Go off.temptest(cli)Review fixes and merges from
developfollow these commits.User-facing reference:
apps/cli/docs/stack-commands.mdand thestartside effects.