Tensile Docs
Concepts

How it works

The architecture behind a clone — CLI, contract, providers, shared infra, and templates.

Tensile Environments have four moving parts. Understanding how they fit makes every command predictable.

        you ──▶ hone 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 hone env command group. 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. Tensile Environments run 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-a1b2 → clone_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

hone env 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 (hone env spin slack stripe) and can drop them straight into a named environment with --group.

Next: Clones, environments, and the registry and Environments.

On this page