carlos / platform Public

CARLOS apps do not have to be written in Go Idea open

The original set of CARLOS apps chose Go: Eleven, Kass, Woodstar, Seapointish, Keymail, Tito. The technical blueprint opens with a section headed "Go server". This could mistakenly be taken as a rule.

It's not. CARLOS binds an app to a protocol. The protocol doesn't care what compiled it.

What the platform actually checks

Five things, none of them a language:

  1. One static binary with no runtime dependencies. A deploy is a single file you copy to the box with scp.
  2. It cross-compiles to linux/arm64 and is ideally small enough for a t4g.nano, Amazon's smallest ARM server, which has 512MB of memory.
  3. It speaks the CARLOS activation contract: the argv shapes, socket handoff through LISTEN_FDS (systemd's mechanism for passing an already-open network socket into a process), a $STATE_DIRECTORY variable, a /healthz endpoint, and GET /api/version reporting the build's git commit. Deploys poll that last one on every instance to confirm the build actually took. It drains on SIGTERM instead of dropping requests in flight.
  4. SQLite on local disk, in-process, with no separate database server, so Litestream can stream the file to S3 as it changes.
  5. It runs under a hardened systemd unit, with no containers.

Clear those and you have a CARLOS app, language-agnostic.

What leaving Go actually costs

The reason every app so far is Go is Rastrillo, not the contract. Rastrillo supplies the parts you would otherwise build by hand, and they are mostly the parts that hurt when you get them wrong. Sessions live in database rows, so revoking one works. Magic-link sign-in upgrades itself to Keymail, the family's shared identity provider, when an address has a claimed inbox. Cross-site request forgery is blocked by checking the request's origin. Queries are owner-scoped, and a row you don't own returns 404 instead of 403, so an attacker can't confirm it exists. The crypto core ships with golden vectors — fixed inputs and expected outputs that its JavaScript twin has to reproduce exactly. Then event logging, file storage, mail, and the activation layer itself.

Rastrillo provides most of a typical app by line count and nearly all of its security-sensitive surface. It exists because the family worked out, more than once, that these are the parts nobody should have to get right twice. Leave Go and you leave Rastrillo, and all of it has to be rebuilt to the same standard in whatever you picked instead.

The proposed rule

Go remains the default. The burden of proof sits on the alternative. Reach for another language when your app's hard part is genuinely better served there, and publish the deviation and your reasoning — the same discipline the family already applies when an app softens its trust model.

Plumbing is not difficulty. If the hard part of your app is authentication, sharing, storage and CRUD screens, the answer is Go, because that is what Rastrillo already solved.

One refinement is worth stating on its own. Usually it's the engine inside an app that wants a different language while the app around it doesn't. Embed that engine in the Go binary rather than running it as a sidecar process: a second process breaks "one file you scp" and doubles the systemd and hibernation bookkeeping for every instance.

Choose the appropriate tech for the hardest problems.

CARLOS Docs, as a test

Docs is being designed now, and it is a fair test of the rule. Its hard parts are a CRDT — a data structure that lets several people edit one document at once without a central lock — and a text layout engine.

The reference implementation of Yjs, the most mature CRDT for this, is y-crdt, and it is Rust. The Go ports are young, third-party, and not what other implementations get tested against. Rust's text-shaping stack (rustybuzz, swash, parley, cosmic-text) is production-grade, where Go's x/image/font/sfnt reads font metrics and cannot shape complex scripts at all.

So Docs uses Go and Rastrillo for the app itself — accounts, sharing, storage — and writes the CRDT and the layout engine once, as a Rust crate compiled to WebAssembly. That crate runs in the browser and also inside the Go server binary, through wazero, a WebAssembly runtime written in pure Go with no C dependency. One compiled engine on both sides, instead of two implementations kept in step by golden vectors. Still one static binary.

Other candidates

Not a closed list. These are the ones worth naming now.

| Language | Where it wins | What it costs | |---|---|---| | Go | The default: static binaries, cgo-free SQLite, Rastrillo, and the experience the family already has | — | | Rust | Parsers, CRDTs, typography, crypto. Static binaries against musl, no runtime | Rebuilding Rastrillo's security-critical parts from scratch | | Zig | Small binaries and unmatched C interop, when you have to link an existing C library | Pre-1.0 churn, and you hand-roll HTTP, TLS, SQLite and crypto | | OCaml | Apps that are really compilers: parsers, query planners, rule engines | Thin web and infrastructure ecosystem, awkward ARM cross-compilation | | Swift | An Apple client sharing a core with the server | Weak Linux server story, large binaries, painful cross-compilation | | C++ | Wrapping an engine you can't reimplement, like a codec or a solver | Everything else, and wrapping it from Go breaks the cgo-free rule | | Mojo | Worth watching | No web or SQLite ecosystem yet, and unresolved maturity |

What this needs from Paul

A ruling. If the rule above is accepted, the blueprint's "Go server" section should become "the app binary", with the five requirements above stated as the actual contract and Go named as the default instead of left as an assumption.

Triage, 2026-09-05. This one needs nothing built. It needs a ruling, and it has been waiting on one.

Recommending accept as written, for one reason above the others: the proposed rule is falsifiable and it has already been tested. CARLOS Docs is a real case where the hard parts (a CRDT and a text layout engine) are genuinely better served elsewhere, the burden of proof was discharged with specifics — y-crdt is the reference implementation and the Go ports are young and not what other implementations are tested against; Go's x/image/font/sfnt reads metrics and cannot shape complex scripts at all — and the answer still came out as "Go and Rastrillo for the app, one Rust crate for the engine, embedded via wazero, still one static binary." A rule that produces that answer rather than "rewrite it in Rust" is doing its job.

The refinement about embedding rather than sidecar-ing is the load-bearing part and should not get lost if this is condensed into the blueprint. A second process breaks "one file you scp" and doubles the systemd and hibernation bookkeeping per instance — which is not a style preference, it is the five-item contract in §"What the platform actually checks" failing at item 1.

Two things I would add to the ruling if it is accepted:

Say where the deviation gets published. The rule asks an author to "publish the deviation and your reasoning", and right now there is no named place for that. If it is a discussion in the app's own repo, say so; otherwise the rule has a step that quietly does not happen.

Rastrillo's absence is the real cost and the table under-prices it. The "what it costs" column for Rust says "rebuilding Rastrillo's security-critical parts from scratch", which is accurate but reads like effort. It is more than effort: sessions as revocable rows, origin-checked CSRF, owner-scoped queries that 404 rather than 403 so an attacker cannot confirm a row exists, and a crypto core with golden vectors its JavaScript twin has to reproduce. Those are decisions the family has already made correctly, once. Leaving Go means making them again, under time pressure, in a language where the reviewer has less practice. Worth a sentence in the blueprint, because "Go is the default" is much easier to hold when the reason is written down.

Blocked on: a yes/no from Paul, then a blueprint edit turning "Go server" into "the app binary" with the five requirements stated as the contract. Small change, and it unblocks the Docs design that is happening now.