Skip to main content
asymmetric has four moving parts. Understanding how they fit makes every command predictable.
        you ──▶ asymmetric CLI ───────────────┐
                  (apps/cli)                   │ speaks
                       │                        ▼
                       │              @repo/clone-contract
                       │              (CloneProvider, types, errors)
                       ▼                        ▲
              ┌──────── provider ───────┐       │ implements
              │  LocalDockerProvider     │──────┘
              │  (shells to docker)      │
              └──────────┬───────────────┘
                         │ docker compose

        ┌──────────────────────── asym-shared network ──────────────────────┐
        │                                                                     │
        │   shared Postgres            shared Redis        per-clone services │
        │   db: clone_<id>             prefix: <id>:        compose: asym-<id> │
        │                                                                     │
        │   ┌─ slack-a1b2 ─┐   ┌─ stripe-c3d4 ┐   ┌─ … ─┐                      │
        │   │ backend :3001 │   │ backend :3003 │                             │
        │   │ frontend  …   │   │  (api-only)   │                             │
        │   └───────────────┘   └───────────────┘                             │
        └─────────────────────────────────────────────────────────────────────┘

The CLI (apps/cli)

The asymmetric command. Each verb lives in its own commands/* module and calls into a service layer that holds a single provider. Every error funnels through one renderer, so you get an actionable sentence and a meaningful exit code — never a stack trace.

The contract (@repo/clone-contract)

The seam. It defines the CloneProvider interface (create / start / stop / destroy / status / logs / seed / reset), the data shapes (CloneRecord, CloneInstance, CloneStatus, CloneTokens, SeedSpec, Environment, FidelityScore), the TemplateManifest, the on-disk registry shapes, and one named error per failure mode. The CLI depends on the contract, not on any clone. That’s what lets the same commands drive a local Docker engine today and a remote control plane later. See the contract reference.

Providers

A provider is the engine that actually does the work behind each verb.
  • LocalDockerProvider runs inside the CLI and shells out to docker and docker compose. No server, no daemon — this is the default and the only one today.
  • CloudProvider (planned) will be a thin client to a control-plane API, implementing the same interface. Swap the engine, keep the commands.

Shared infrastructure

Spinning a clone does not start a fresh database per clone. asymmetric runs one shared Postgres and one shared Redis on a Docker network called asym-shared, and isolates clones inside them:
  • Each clone gets its own database, named clone_<id> (hyphens in the id become underscores — slack-a1b2clone_slack_a1b2).
  • Each clone gets a Redis key prefix, <id>:.
  • Each clone’s services run as their own compose project, asym-<id>, on host ports drawn from your configured range (default 3000–3999).
This keeps a fleet of clones cheap to run while keeping their data fully separate.

Templates

A template is a directory with a clone.manifest.json that declares the clone’s services, run modes, database/migrations, Redis prefix, and seed sources. Six templates ship today — Slack, Stripe, Notion, HubSpot, GitHub, and Linear. The CLI reads the manifest to know what to run, which ports to expose, where the migrations live, and how to health-check the clone. See Templates. Backends ship as published images (ghcr.io/asymmetric-ai/…), so the CLI pulls and runs them with no source present and pins the resolved digest — that digest pin is what makes reset and re-spin byte-identical even as :latest moves.

What spin does, end to end

asymmetric spin slack runs this sequence:
1

Allocate an id

slack-<random hex>, unique across your registry.
2

Resolve the template and mode

Read clone.manifest.json; validate the requested mode (default: the template’s first mode).
3

Pull the image

Pull the backend’s published image and resolve it to a content digest, which is pinned on the clone so reset/re-spin stay byte-identical.
4

Allocate ports

One host port per service in the mode, from your port range, avoiding collisions with existing clones.
5

Ensure shared infra

Start shared Postgres + Redis on asym-shared if they aren’t up.
6

Create the database and migrate

CREATE DATABASE clone_<id>, then run every .sql migration in order.
7

Compose up

docker compose -p asym-<id> up -d, injecting the database URL, Redis URL/prefix, a random JWT secret, and the bind address. The backend runs from the pinned image — no local build.
8

Wait for health

Poll the backend’s health endpoint until healthy, or fail after 90s.
9

Provision tokens

Call the clone’s bootstrap endpoint to mint a default app and its bot + user tokens (the DB stores only hashes). Templates without a provision surface skip this step.
10

Record it

Write the clone to ~/.asymmetric/registry.json. On any failure, roll back: compose down -v and drop the database, so you never get a half-clone.
spin takes one or more templates (asymmetric spin slack stripe) and can drop them straight into a named environment with --group. Next: Clones, environments, and the registry and Environments.