carlos / platform Public

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>
Paul Campbell 57b20bf7a168dd7b8d72d79cca9f7b71afa977d0
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).