Add CARLOS platform design spec
Concept spec for the deployment platform: bucket-as-control-plane, one carlos binary, channels unifying dev and prod, blind/seeing shared services, placement tiers with residency, mergeable stores, and graduation to attached databases.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+407
−0
docs/superpowers/specs/2026-08-01-carlos-platform-design.md+407 −0
diff --git a/docs/superpowers/specs/2026-08-01-carlos-platform-design.md b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md| new file mode 100644 |
| index 000000000..cf95c54fb |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-08-01-carlos-platform-design.md |
| @@ -0,0 +1,407 @@ |
| +# ๐ค The CARLOS Platform โ Design |
| + |
| +> ๐ค This document was written by an LLM (Claude), on the ideas, instruction, |
| +> and editing of humans, per [factor X](https://11factor.org/#x). |
| + |
| +**Status:** Concept spec, approved in brainstorming 2026-08-01. |
| +**Relationship to CARLOS:** the framework (carlosframework.com) defines the |
| +shape of an app; this document defines the substrate apps run on โ the way |
| +Heroku stood beside the twelve factors. |
| + |
| +## 1. Purpose |
| + |
| +A black-box deployment platform for CARLOS apps. An app author ships |
| +**releases** โ immutable, content-addressed artifacts โ and the platform |
| +places them on **instances**, routes to them through a **mesh**, and |
| +surrounds them with platform-level glue: relays, proxies, shared services, |
| +orchestration. Creating a canary is near-instant; shipping a release that |
| +instances adopt on their own schedule is trivial; and instances from many |
| +apps (Eleven, Keymail, Kass, Tito, a stranger's new app) run side by side on |
| +the same fleet, sharing the same blind infrastructure. |
| + |
| +The platform holds itself to the promises it hosts. It is Cost-efficient |
| +(hosts do nothing for parked instances), Available (a wake is a request away), |
| +Replicated (no host is authoritative), Lightweight (one static binary), Open |
| +(see ยง2), and Secure (the control plane passes the publish-the-database test). |
| + |
| +## 2. Operator model |
| + |
| +Like the apps, the platform is **software first, service second**. There is |
| +one flagship deployment, operated by the CARLOS authors and hosting the |
| +flagship apps โ and the platform is entirely open, and trivially |
| +self-hostable: a stranger stands up their own deployment on their own |
| +hardware. "Anyone can create a CARLOS app" means anyone can build one and run |
| +it on any deployment. |
| + |
| +A **deployment** is the unit of the platform: |
| + |
| +> **one object-store bucket + N hosts running the `carlos` binary + DNS.** |
| + |
| +That sentence is the entire self-hosting story, and keeping it one sentence |
| +is a design constraint on everything below. |
| + |
| +**Tenancy is operator-curated.** Each deployment's operator chooses which |
| +apps it hosts. Apps on a deployment trust the operator; isolation between |
| +apps is hygiene (separate OS users, directories, sockets), not a defense |
| +against hostile code. Nothing in the manifest or mesh design precludes hard |
| +sandboxing later, but **untrusted multi-tenancy is an explicit non-goal of |
| +v1** (ยง10). Until then, placement is the isolation knob (ยง6). |
| + |
| +## 3. Architecture overview |
| + |
| +There is no control-plane server. All durable platform state โ apps, |
| +releases, channels, environment profiles, placement pools, instance records, |
| +leases โ lives in the object store as immutable or lease-guarded objects, |
| +the same way instance databases already park there. Hosts converge by |
| +watching the bucket; the mesh's routing table is derived state, rebuildable |
| +from the bucket; a host can burn down and be replaced by pointing a fresh |
| +box at the bucket. The platform makes about itself the promise instances |
| +already make: the box is disposable, the bucket is not. |
| + |
| +One static `carlos` binary plays every role, selected by configuration: |
| + |
| +- **Edge** โ the mesh's front door. Terminates TLS, owns identity and |
| + sessions (the blind router, promoted to platform level), routes requests |
| + to instances anywhere in the deployment. Knows *where* things are, never |
| + *what* they contain. |
| +- **Host agent** โ spawns, hibernates, parks, and wakes instances on its |
| + box; watches the bucket for channel advances and rolls instances onto new |
| + releases; enforces placement and residency at the lease-claim (ยง6). |
| +- **CLI** โ what an app author holds: `carlos ship`, `carlos canary`, |
| + `carlos promote`, `carlos graduate`, `carlos vet`. Talks to the bucket, |
| + not to a server. |
| + |
| +Two derived components, both ordinary CARLOS apps hosted *on* the |
| +deployment and never sources of truth: a **console** (dashboard โ a |
| +read-model of the bucket) and the shared services (ยง7). |
| + |
| +State hierarchy in the bucket: |
| + |
| + deployment |
| + โโโ apps |
| + โ โโโ releases (immutable, content-addressed) |
| + โ โโโ channels (lease-guarded pointers to releases) |
| + โ โโโ instances (records, leases, streams, parked databases) |
| + โโโ environment profiles (glue binding sets: sandbox, production, โฆ) |
| + โโโ placement pools (shared, dedicated pools; host regions) |
| + |
| +## 4. Apps, releases, and channels |
| + |
| +### The manifest |
| + |
| +An **app** is a name plus a **manifest** โ the entire contract between |
| +author and platform. It declares: |
| + |
| +- **Processes**, each with a serving class (ยง5): `routed`, `replicated`, or |
| + `sidecar`. |
| +- **Storage**, per process: ownership (`exclusive` | `mergeable`) and |
| + engine (`embedded` | `attached`) (ยง5). |
| +- **Placement**: tier (`shared` | `dedicated`), and optional residency |
| + constraints (ยง6). |
| +- **Service needs**: which shared services it consumes, each tagged with |
| + the class it requires (`blind` | `seeing`), and each marked `required` or |
| + `degradable` (ยง7, ยง8). A dependency on a seeing service is visible in the |
| + manifest, not discovered in traffic. |
| +- **Routing keys**: how the edge maps a request to an instance (hostname, |
| + path prefix, invite-token namespace). |
| + |
| +### Releases |
| + |
| +A **release** is immutable and content-addressed: one static binary per |
| +platform target, the manifest, and a build attestation (commit, builder). |
| +`carlos ship` uploads it under its hash. Releases are never modified; every |
| +environment running release `abc123` runs byte-identical code. |
| + |
| +### Channels |
| + |
| +A **channel** is a per-app named pointer to a release: `stable`, `beta`, |
| +and ephemeral `canary/*` channels. Advancing a channel is one lease-guarded |
| +pointer write. Instances subscribe to a channel; host agents see the pointer |
| +move and roll instances onto the new release **at their next natural |
| +boundary** โ wake from hibernation, or a drain-and-restart sweep for |
| +long-running instances. Rollback is the same operation backwards. |
| + |
| +Because instances adopt on wake and CARLOS migrations are additive-only, a |
| +fleet with three releases live at once is a normal, safe state. |
| + |
| +**Dev and prod are not separate systems.** A dev canary is an ephemeral |
| +channel with one subscriber. The flow, from a worktree: |
| + |
| + carlos canary โ build โ ship โ create channel canary/<branch> |
| + โ spawn one instance subscribed to it, |
| + bound to the sandbox environment profile |
| + โ return its URL (seconds, not minutes) |
| + |
| + carlos promote โ republish the same hash to a wider channel |
| + (canary โ beta โ stable; no rebuild, ever) |
| + |
| +The dev pipeline *is* the prod pipeline, exercised on every push. |
| + |
| +### Environment profiles |
| + |
| +Environments are configuration, not architecture. An **environment |
| +profile** is a named binding set resolving each declared service need to a |
| +concrete endpoint: canaries bind to sandbox glue (throwaway bucket |
| +partition, test OAuth relay), stable instances bind to production glue. |
| +One baked-in guard rail: profiles are isolated โ a canary cannot reach |
| +production glue by accident, enforced at binding resolution, not by |
| +convention. |
| + |
| +## 5. Serving and storage |
| + |
| +### Serving classes |
| + |
| +- **`routed`** โ one sticky instance per routing key; the CARLOS shape. |
| + Blast radius of one; hibernates; parks. |
| +- **`replicated`** โ N interchangeable copies. Two cases the manifest |
| + distinguishes: *stateless* processes (edge round-robins, scaled by simple |
| + demand rules) and *read replicas* (tail the writer's WAL stream from the |
| + bucket; serve reads; every write routes to the writer). |
| +- **`sidecar`** โ a headless client the platform happens to host; ยง5.4. |
| + |
| +### Storage: ownership |
| + |
| +- **`exclusive`** โ one database under the single-writer lease, where the |
| + lock and the manifest are one object (torn-state protection inherited |
| + from CARLOS). Replication never touches this invariant: read replicas |
| + are read-only, and there is no multi-writer mode for mutable stores. |
| +- **`mergeable`** โ for append-only-style apps (Eleven's shape). |
| + "Multi-writer" is structured as **many single-writer streams plus a |
| + deterministic merge**: each instance owns its own append-only stream in |
| + the bucket; visible state is the merge of all streams โ union of events |
| + under a causal/lamport ordering, platform-default or app-supplied. Every |
| + stream still has exactly one writer, so leases, parking, and takeover |
| + are unchanged; there are just N leased streams per dataset. This yields |
| + **write-anywhere replication**: several live instances of one dataset, |
| + each appending locally, all converging โ conflict-free by construction |
| + rather than by coordination. The manifest only accepts `mergeable` for |
| + processes declaring an append-only write model, and the platform never |
| + merges an `exclusive` store. |
| + |
| +### Storage: engine, and the graduation path |
| + |
| +- **`embedded`** โ SQLite compiled into the binary, parked to the bucket |
| + when idle. The default; the only engine a fresh app needs. |
| +- **`attached`** โ a server database (Postgres/MySQL) provisioned by the |
| + platform and bound via the environment profile. An attached database is |
| + **leased infrastructure inside the app's trust domain** โ it holds |
| + plaintext app data, so it is emphatically not shared blind glue: it runs |
| + in the app's placement pool, one database per instance, never |
| + multi-tenant across apps. |
| + |
| +**Graduation** is per-instance: `carlos graduate <instance>` drains the |
| +instance, exports the embedded database, provisions the attached one, |
| +replays the additive migration history onto the new engine, imports, and |
| +restarts bound to it. The cost stays honest and with the author: their code |
| +must run against every engine the manifest declares, enforced by a test |
| +gate (`carlos vet`, ยง9), not a paragraph. Graduated instances become |
| +**resident**: no parking โ the database server, with its own |
| +platform-managed replication and backups, is now the durability story โ |
| +though hibernating the process while the database idles remains available. |
| + |
| +The platform covers the whole scale curve, and an app moves along it by |
| +changing declarations, not architecture: |
| + |
| + parked (zero) โ embedded (one process) โ replicated/mergeable (out) |
| + โ attached (up) โ dedicated pool (isolated) |
| + |
| +### Sidecars |
| + |
| +A **sidecar** is not routed (nothing routes to it) and not a service (it |
| +holds a private key and sees plaintext for conversations it was invited |
| +to). It is a headless client: |
| + |
| +- **Outbound-only.** No routing keys, no listening socket, unreachable |
| + from the mesh. It *dials* the edge as its member identity and receives |
| + over that connection. Because the contract is "dials out, nothing dials |
| + in," the identical artifact runs off-platform โ a laptop behind NAT, a |
| + home server โ with zero protocol difference. Platform hosting is a |
| + convenience, never a requirement: an agent's key never *has* to touch |
| + platform hardware. |
| +- **Own trust domain.** When platform-hosted, it runs in the app's |
| + placement pool as app code, its key material provisioned by the operator |
| + through the vault, sealed so only that sidecar's instance can open it. |
| +- **Hibernated by default; every wake has a named reason.** Wake sources: |
| + **delivery** (the edge sees โ metadata only โ ciphertext queued for the |
| + sidecar's identity and signals the host agent), **schedule** (timers |
| + registered with the host agent before sleeping, or a cron line in the |
| + manifest), or **operator command**. A bot receiving three messages a day |
| + costs three wakes a day. |
| + |
| +The class is also the general home for any worker that consumes a stream |
| +but serves no requests: digest senders, indexers, schedulers. |
| + |
| +## 6. Placement: pools, tiers, residency |
| + |
| +Hosts belong to **pools**: the `shared` pool and named dedicated pools. |
| +Hosts carry a **region** attribute. |
| + |
| +- **Shared tier** โ instances co-reside with other apps' instances on |
| + fleet hosts. The default; fine for E2EE apps, whose hosts hold only |
| + ciphertext regardless. |
| +- **Dedicated tier** โ the app's instances, parked data, and wake path |
| + only ever touch hosts reserved for it, at whole-VPS cost. This is the |
| + isolation story for apps that are not E2EE and hold real personal or |
| + payment data (the Titogo case): until hard sandboxing exists, such an |
| + app should not share a kernel with other tenants' code, even curated. |
| + A dedicated pool may also use **its own bucket**, so the app's data |
| + never rests in shared object storage โ while still riding the shared |
| + edge, channels, CLI, and services. |
| +- **Residency** โ an app, or a single instance (a tenant-level choice: |
| + one customer may demand EU while another doesn't care), declares a |
| + residency constraint (`residency: eu`). It binds both halves of the |
| + instance's life: |
| + - *Execution:* a host agent whose region is outside the constraint will |
| + not claim the lease. The wake protocol is the enforcement โ there is |
| + no code path where an EU instance runs elsewhere "just this once |
| + during a failover." No eligible host up โ the wake fails loudly. |
| + - *Rest:* the parked database, WAL stream, and blobs live in a |
| + region-scoped bucket partition or separate regional bucket. Residency |
| + covers data at rest, not just compute. |
| + |
| + The edge stays global โ routing metadata is not resident data โ but |
| + request bodies for a constrained instance terminate only at in-region |
| + hosts. Residency composes with pools by intersection: |
| + `pool: titogo, residency: eu`. |
| + |
| +**Scheduling is deliberately dumb.** An instance wakes on whichever |
| +eligible host answers first, weighted by load. No bin-packing optimizer: |
| +hibernation makes placement mistakes cheap โ the instance parks and can |
| +wake elsewhere next time. |
| + |
| +## 7. Shared services |
| + |
| +Shared services are how "focus mainly on the app" becomes true. Every |
| +service carries a declared **class**, visible in the manifest of every |
| +consumer: |
| + |
| +**Blind services** โ may hold data, but only data they cannot read: |
| + |
| +| Service | Holds | Never sees | |
| +|---|---|---| |
| +| Object store (mediated bucket) | parked DBs, releases, blobs | plaintext of E2EE apps | |
| +| Secret storage / vault | sealed blobs | the keys, which live client-side or in app instances | |
| +| Push relay | device tokens, timing | notification content (opaque payloads to APNs/FCM/WebPush) | |
| +| OAuth relay | redirect plumbing | retained tokens โ tokens are sealed to the requesting instance's key on arrival, relayed, not stored | |
| + |
| +**Seeing services** โ process plaintext, therefore forbidden to hold it: |
| + |
| +| Service | Does | Guarantee | |
| +|---|---|---| |
| +| Unfurler | fetches a URL, returns preview metadata; also the audited egress choke point (instances need no arbitrary outbound network) | stateless | |
| +| PDF / render | takes plaintext content, returns a document | stateless, forgets it | |
| + |
| +The enforceable line: **nothing that sees may store.** A seeing service is |
| +stateless *by construction* โ the platform grants it no database binding, |
| +no bucket write access, no persistent volume. The property is imposed, not |
| +promised. Blind services may store; what makes them blind is falsifiable |
| +the CARLOS way: publish their state and see what leaks. |
| + |
| +**Mechanics.** Services are CARLOS apps in a reserved namespace (`svc/*`), |
| +shipped through the same channels โ a service canary works exactly like an |
| +app canary. Apps consume services through their environment profile: the |
| +manifest names the need, the profile binds the endpoint, and the mesh |
| +attaches the calling instance's identity to each call โ so services |
| +rate-limit per tenant, and dedicated-tier apps can bind to dedicated |
| +service replicas when even transient co-processing is unacceptable. |
| + |
| +Anyone can publish a service โ it is an app with a class declaration โ but |
| +the class claim is reviewed at adoption like any curated tenant, and for |
| +seeing services, mechanically enforced regardless. |
| + |
| +## 8. Failure handling |
| + |
| +Every failure resolves by re-deriving state from the bucket, and every |
| +recovery path is the normal path โ wake, adopt, re-lease โ exercised |
| +thousands of times a day, not a dusty runbook. |
| + |
| +- **Host dies.** Its leases expire; the next request or scheduled wake |
| + finds them free and the instances wake elsewhere from their last parked |
| + state. The exposure window is writes since the last stream flush โ the |
| + window CARLOS already accepts, kept small by continuous WAL streaming. |
| + Mergeable datasets fare better: other instances keep accepting writes |
| + throughout. |
| +- **Edge dies.** Edges are replicated and share nothing not derived from |
| + the bucket; DNS/anycast shifts traffic. Sessions ride with identity, not |
| + with an edge process. |
| +- **Bucket unreachable.** The one true dependency, chosen deliberately โ |
| + object storage is the most durable commodity available. Degradation is |
| + honest: running instances keep serving from their local working set but |
| + the platform refuses transitions (no park, wake, graduate, or ship) |
| + rather than guessing. Bucket *loss* is deployment loss, so the bucket is |
| + the one place redundancy is bought: cross-provider replication as a |
| + platform feature. The box is disposable; the bucket is not. |
| +- **Bad release.** The blast pattern is the rollout pattern: adopt-on-wake |
| + makes a poisoned release hit gradually; channel rollback (pointer to the |
| + previous hash) stops the bleed, and adopters re-adopt backwards on next |
| + wake. Data damage is bounded by additive-only migrations (old data stays |
| + readable) and by parked snapshots, which are point-in-time copies that |
| + exist by construction. |
| +- **Attached database fails.** The standard managed-database story โ |
| + per-engine replication and backups โ provisioned by the platform, |
| + explicitly not reinvented by it. |
| +- **Shared service down.** `required` needs fail the request; `degradable` |
| + needs fail fast and the app runs without (no unfurls today; messages |
| + still flow). Seeing services, being stateless, are restart storms at |
| + worst. |
| + |
| +## 9. Testing and guard rails |
| + |
| +Assertions that fail builds, not paragraphs: |
| + |
| +- **The platform is a CARLOS app.** One repo, one static binary, line-cap |
| + tests, no build step for the console front end, additive-only migrations |
| + for anything stateful. |
| +- **The bucket schema is the API โ contract-tested.** A versioned suite |
| + pins the object layout: binary N reads a bucket written by Nโ1 and |
| + refuses, loudly, one written by N+1. This is what makes "point a fresh |
| + binary at an old bucket" safe forever. |
| +- **Invariants get adversarial tests.** Lease exclusivity under concurrent |
| + takeover; mergeable convergence (N writers, randomized interleavings, |
| + one final state); residency (a wrong-region wake attempt must fail); |
| + seeing-service statelessness (the harness checks: no volume, no DB |
| + binding, no bucket write grant). |
| +- **Every platform change ships through the platform.** Worktree โ canary |
| + channel โ a miniature canary deployment (three hosts, own bucket) โ |
| + the headless-browser gate failing on any console error. Review happens |
| + on something deployed, never localhost. The release machinery is tested |
| + by being the only way anything ships, including itself. |
| +- **`carlos vet` โ the app conformance suite.** Runs an artifact against |
| + the platform contract: manifest validity; engine claims (boots against |
| + every engine it declares); additive-migration check between its last two |
| + releases; park/wake round-trip. Passing vet makes an app's promises |
| + checkable before adoption rather than discovered in production. |
| + |
| +## 10. Non-goals (v1) |
| + |
| +- **Untrusted multi-tenancy.** No sandboxing story for hostile code; |
| + tenancy is operator-curated. The manifest and mesh must not preclude |
| + hard sandboxing later, but it is not built now. |
| +- **Multi-writer mutable stores.** `mergeable` is for append-only models |
| + only. No CRDT-general merging of mutable rows, no consensus layer. |
| +- **Reinventing managed databases.** Attached engines use their native |
| + replication and backup machinery; the platform provisions and binds, |
| + nothing more. |
| +- **Bin-packing schedulers.** Dumb placement is a feature; hibernation |
| + makes it cheap. |
| +- **Cross-deployment federation.** A stranger runs their own deployment; |
| + deployments do not (yet) share services or identity with each other. |
| + |
| +## 11. Open questions |
| + |
| +1. **Eleven's merge rule.** Does Eleven's existing multi-write design |
| + match the per-writer-stream-plus-union shape, and is its ordering rule |
| + simple enough to be the platform default, or does it stay app-supplied? |
| +2. **Graduation reversibility.** Is `carlos graduate` one-way in v1, or is |
| + de-graduation (attached โ embedded, for a tenant that shrank) worth its |
| + complexity? |
| +3. **Naming.** The flagship deployment, the `carlos` binary's role names, |
| + and whether `svc/*` services get their own brand. |
| + |
| +## 12. Provenance |
| + |
| +Extracted from the systems that keep rebuilding this shape โ Eleven, |
| +Keymail, Woodstar, Slopbox, Kass, Tito โ and designed in conversation on |
| +2026-08-01. The framework's promises live at |
| +[carlosframework.com](https://carlosframework.com); the factors it keeps at |
| +[11factor.org](https://11factor.org). |