Skip to content
Reference

.stack.toml schema

The two-file config split — what's committed, what's local, and why.

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. Your project_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.

Legacy single-file configs: If Stack finds a single .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 secrets entry without running stack env diff — you'll end up with orphan keys in Phantom.
  • Don't add an mcp name Stack doesn't know about.mcp.json only gets regenerated when stack add or stack remove runs, 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.