Clones
A clone is one running instance of a template. It has:
| Field | Example | What it is |
|---|
id | slack-a1b2 | Stable handle. You pass it to every command. <template>-<random hex>. |
template | slack | Which template it came from. |
mode | api | Which set of services is running (see below). |
composeProject | asym-slack-a1b2 | The Docker Compose project for its containers. |
database | clone_slack_a1b2 | Its own database inside shared Postgres. |
redisPrefix | slack-a1b2: | Its key namespace inside shared Redis. |
ports | { backend: 3001 } | Host ports per service. |
state | running | Lifecycle state (see below). |
seed | { fixture: "acme-corp" } | The last seed applied — replayed on reset. |
tokens | { botToken, userToken, … } | App credentials minted at spin (when the template provisions them). |
image / imageDigest | …slack-backend:latest @ sha256:… | The published image and pinned digest the clone runs from. |
expose | false | Whether services bind 0.0.0.0 or localhost-only. |
A clone does not store which environment it belongs to. Membership lives on the
environment (Environment.members) as the single source
of truth; status derives a clone’s environment by reverse lookup.
Modes
A template declares modes — named sets of services. The first declared mode is
the default. Every template ships an api mode (backend only — the lightest
clone, enough for an agent that talks to the API or MCP server). The Slack
template additionally ships a full mode that adds the frontend UI; the
API-only templates (Stripe, Notion, HubSpot, GitHub, Linear) have api alone.
Pick one with --mode:
asymmetric spin slack --mode full
Omit it and you get the template’s first declared mode.
Tokens
When a clone’s template has a provision surface (Slack today), spin calls its
bootstrap endpoint once the backend is healthy and mints a default app with a
bot token and a user token. They’re shown once at spin time and stored in
the registry (the clone DB keeps only hashes). Reprint or rotate them with
asymmetric tokens <id>. Templates without a provision surface
simply have no tokens.
States
A clone moves through a small state machine:
creating ──▶ running ──▶ stopped ──▶ running ──▶ destroying ──▶ gone
│
└──▶ unhealthy / failed
| State | Meaning |
|---|
creating | Being built and started. |
running | Healthy and serving. |
unhealthy | Containers up but the health check is failing. |
stopped | Containers stopped, data preserved (asymmetric stop). |
failed | Spin or a lifecycle step errored. |
destroying / gone | Being torn down / removed. |
asymmetric doctor flags clones in non-terminal states that need a live check.
Environments
Clones compose into environments — the top-level
primitive you point an agent at and score. An environment has a name, a member list
(the source of truth for membership), and a rolled-up state
(running / partial / stopped / failed). Spin clones straight into one:
asymmetric spin slack stripe --group acme # both clones join environment "acme"
Or declare them in an environment.yaml and asymmetric env spin it. See
Environments for the full story.
The registry
Everything above is persisted to ~/.asymmetric/registry.json — a versioned JSON
file holding clones and environments, rewritten on every
create / destroy / reset / seed / start / stop. It’s plain JSON and
safe to read (tokens are redacted from status/ls):
The schema is versioned and migrated on read: opening an older registry upgrades it
in place (v1 → v2 renamed groups to environments and rebuilt environments from
any legacy per-clone group tags, so no membership is lost on upgrade).
Because the registry is the source of truth, status, doctor, and db work
without touching Docker at all — they just read it. If it ever gets corrupted,
asymmetric doctor is the recovery path.
Override the registry location with ASYM_HOME. Point CI at a throwaway
directory so its clones never collide with your local ones.