Why two files
Stack splits its config across two TOML files because one file can't serve both jobs.
-
.stack.toml(committed) — the shape of the stack. Which services this project uses, what their canonical secret slot names are, which MCP servers to wire. Safe to share in git. -
.stack.local.toml(gitignored automatically) — the instance. Yourproject_id, the provider-side resource ids (Supabase project ref, Vercel project id), regions you picked, timestamps.
When you run stack init, both files are created and Stack
appends .stack.local.toml to your .gitignore. A
teammate who clones your repo gets the shape file only — their
stack add calls provision their own resources and populate
their own .stack.local.toml.
.stack.toml with instance fields at the top level
(project_id, created_at, resource_id),
it reads it transparently as a merged config. The next write splits it into
the two-file layout.
Committed shape — .stack.toml
Complete example:
# Ashlr Stack — committed shape. Defines which services this project uses.
# Safe to share in git; secret *values* live in Phantom, not here.
[stack]
version = "1"
template = "nextjs-supabase-posthog"
[services.supabase]
provider = "supabase"
secrets = ["SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY"]
mcp = "supabase"
[services.posthog]
provider = "posthog"
secrets = ["POSTHOG_PERSONAL_API_KEY"]
mcp = "posthog"
[[environments]]
name = "dev"
default = true
[[environments]]
name = "prod" Fields
| Field | Type | Description |
|---|---|---|
[stack].version | string | Schema version. Currently "1". Written automatically. |
[stack].template | string? | Optional name of the template this config was seeded from. Used by stack deps and stack projects list. |
[services.<name>].provider | string | The provider key (supabase, neon, etc.) — see Providers. |
[services.<name>].secrets | string[] | Canonical .env names this service owns. Stack stores the values in Phantom under these exact keys. |
[services.<name>].mcp | string? | When set, the name of the MCP server entry Stack maintains in .mcp.json for this service. |
[[environments]] | array | One per environment (e.g. dev, prod). Exactly one should have default = true. |
[[environments]].overrides | table? | Per-env secret overrides — mostly unused in v1, wired for later. |
Local instance — .stack.local.toml
Gitignored. Holds everything that's per-developer or per-machine. Example:
# Ashlr Stack — local instance data. Auto-generated; do not commit.
[stack]
project_id = "stk_8f3c6a2e"
[services.supabase]
resource_id = "abcdefghijklmnop"
region = "us-east-1"
created_at = "2026-03-14T09:21:14.211Z"
created_by = "mason@evero-consulting.com"
[services.posthog]
resource_id = "1234"
created_at = "2026-03-14T09:21:27.004Z" Fields
| Field | Type | Description |
|---|---|---|
[stack].project_id | string | Stack-generated id (stk_<12 hex>). Used by the cross-project registry at ~/.stack/projects.json. |
[services.<name>].resource_id | string? | Provider-side resource id (Supabase project ref, Vercel project id, Fly app name, ...). |
[services.<name>].region | string? | Region the resource was created in. Can differ per developer. |
[services.<name>].meta | table? | Freeform provider-specific fields (org slugs, account ids, pricing plans). |
[services.<name>].created_at | string | ISO-8601 timestamp. |
[services.<name>].created_by | string? | Whoever ran stack add — usually an email from the provider's identity response. |
The merged view in code
Inside the core package, readConfig() merges both files into a
single StackConfig. This is the shape CLI commands and the MCP
server operate on:
export interface StackConfig {
stack: {
version: string;
project_id: string;
template?: string;
};
services: Record<string, {
provider: string;
secrets: string[];
mcp?: string;
resource_id?: string;
region?: string;
meta?: Record<string, unknown>;
created_at: string;
created_by?: string;
}>;
environments: Array<{
name: string;
default?: boolean;
overrides?: Record<string, string>;
}>;
} What doesn't live here
Deliberately absent from both files:
- Secret values. Never stored on disk. Live in Phantom's encrypted vault, referenced by slot name.
- OAuth access tokens. Also in Phantom (one slot per
provider — e.g.
SUPABASE_STACK_PAT,GITHUB_TOKEN). - User-facing display names. Those live in the provider
definitions in
packages/core/src/providers/*.ts, not in per-project config.
Editing by hand
You can edit .stack.toml by hand — it's just TOML — but the
CLI is the supported path. Common gotchas if you do edit:
- Don't remove a
secretsentry without runningstack env diff— you'll end up with orphan keys in Phantom. - Don't add an
mcpname Stack doesn't know about —.mcp.jsononly gets regenerated whenstack addorstack removeruns, and both expect the MCP entry to match a provider. - Don't commit
.stack.local.toml. Stack auto-gitignores it, but a force-add will slip through.