This document describes how CLI-owned state is split between the repo-local .supabase/
directory and the global SUPABASE_HOME.
By default:
SUPABASE_HOME = ~/.supabase
The path can be overridden with the SUPABASE_HOME environment variable.
- keep committed project intent in
supabase/ - keep checkout-specific machine state explicit and discoverable in
.supabase/ - keep machine-global auth, telemetry, and binary caches in
SUPABASE_HOME - keep live runtime socket state under the OS temp directory
Project-scoped local state lives next to the repo as a gitignored sibling of supabase/:
<project-root>/
supabase/
config.json
migrations/
functions/
.supabase/
project.json
local-versions.json
stacks/
default/
stack.json
state.json
data/
This state is:
- local to one checkout
- readable by humans and agents directly from the repo root
- intentionally not committed
Machine-global state remains under SUPABASE_HOME:
~/.supabase/
access-token
telemetry.json
traces/
<date>.ndjson
bin/
<service>/
<version>/
<platform>/
...
This state is shared across all local projects on the machine.
For project-local CLI state, the CLI resolves the active project root from cwd using this
order:
- nearest ancestor containing
supabase/config.tomlorsupabase/config.json - otherwise nearest ancestor containing
.supabase/project.json - otherwise
cwd
That means:
supabase linkandsupabase unlinkcan work beforesupabase initsupabase start,supabase stop,supabase status,supabase stack list, andsupabase logscan be run from nested subdirectories inside a linked checkout- stack persistence is no longer keyed by a hashed global project directory
@supabase/config still only discovers supabase/config.*. The broader .supabase/project.json
fallback is CLI-specific runtime behavior.
.supabase/project.json stores cached linked-remote metadata for the checkout.
Shape:
{
"ref": "abcdefghijklmnopqrst",
"name": "my-project",
"fetchedAt": "2026-03-25T12:34:56.000Z",
"versions": {
"postgres": "17.6.1.084",
"postgrest": "14.4",
"auth": "2.188.1",
"storage": "1.43.3"
}
}This file is written by supabase link and removed by supabase unlink.
It is CLI runtime state, not committed project config. The linked project ref does not live in
supabase/config.json.
.supabase/local-versions.json stores optional checkout-local service version overrides.
Shape:
{
"updatedAt": "2026-03-23T10:15:00.000Z",
"versions": {
"auth": "2.180.0",
"storage": "1.39.2"
}
}This is a power-user escape hatch. There is no dedicated top-level command for it yet. Advanced users can edit it directly if they want persistent local overrides.
Each project can own multiple named local stacks:
.supabase/stacks/
default/
preview/
ci/
The implicit stack name is default.
stack.json is the durable per-stack metadata record. It stores:
schemaVersionupdatedAtports- the pinned baseline
servicesmanifest for that stack lastNotifiedUpdateFingerprintwhen the CLI has already warned about available updates
state.json is the live runtime record for a running stack. It stores connection info,
service endpoints, process identifiers, and the exact service versions currently running.
It is written when the managed stack is running and removed on normal supabase stop.
data/ stores persisted local service data for that stack.
The CLI does not currently persist stack logs under .supabase/; logs are buffered in memory by
the daemon and streamed on demand through supabase logs.
There are two separate concepts:
- the candidate baseline, computed from cached linked-remote versions plus CLI defaults
- the pinned baseline, stored in
.supabase/stacks/<name>/stack.json
The candidate baseline is:
- cached linked service versions from
.supabase/project.json - CLI
DEFAULT_VERSIONSas fallback for everything else
The pinned baseline is what a named stack actually uses by default on subsequent starts.
Runtime precedence is:
- per-run
supabase start --service-version service=version - checkout-local overrides from
.supabase/local-versions.json - pinned stack versions from
.supabase/stacks/<name>/stack.json
If a stack has never been started before and stack.json does not exist yet, the CLI creates it
from the current candidate baseline.
This keeps linked remote parity, persistent local experimentation, and one-off overrides separate from committed project config.
supabase init creates a minimal repo-scoped config file:
supabase/config.json
with only a top-level "$schema" reference:
{
"$schema": "https://supabase.com/docs/cli/config.schema.json"
}It does not link a remote project and does not create .supabase/project.json.
supabase link binds the local project to a remote Supabase project and refreshes the cached
linked metadata in .supabase/project.json.
If the linked remote service versions differ from any existing pinned stack metadata, link
warns and tells the user to run supabase stack update.
supabase stack update is the explicit adoption step for pinned local stack versions.
When the project is linked, it first fetches the latest remote service versions and rewrites
.supabase/project.json. It then recomputes the candidate baseline and writes the pinned stack
versions into .supabase/stacks/<name>/stack.json.
If the stack is currently running, update warns that the user must stop and start it again for
the new pinned versions to take effect.
supabase stack status is local-only. It does not make a network call.
It shows:
- a detailed running view when
state.jsonexists and the daemon is alive - a detailed stopped view when only
stack.jsonexists - whether pinned stack versions are up to date against the current candidate baseline
supabase stack list scans .supabase/stacks/*/stack.json for the current project and overlays
live state.json data when a daemon is running.
Not all runtime files live in the repo.
Auth is still machine-global today:
- keyring entry:
Supabase CLI/access-token - filesystem fallback:
<SUPABASE_HOME or ~/.supabase>/access-token
Telemetry state remains in SUPABASE_HOME:
telemetry.jsontraces/
Downloaded binaries remain shared across projects in:
<SUPABASE_HOME or ~/.supabase>/bin/
The legacy Go installer stores its Deno binary directly under the state root:
<SUPABASE_HOME or ~/.supabase>/deno
Managed daemon runtime directories, including the live Unix socket path, still use the OS temp directory:
/tmp/supabase/
The durable stack record remains in the repo-local state directory:
<project-root>/.supabase/stacks/<stack-name>/stack.json
When deciding where something belongs, use this rule of thumb:
- user-authored project config belongs in the repository under
supabase/ - checkout-specific machine state belongs in
.supabase/ - machine-global auth, telemetry, and caches belong in
SUPABASE_HOME - live runtime temp/socket state belongs under the OS temp directory