carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1---
2name: building-carlos-apps
3description: Use when building or designing an app on the CARLOS architecture (Cost-efficient, Available, Replicated, Lightweight, Open, Secure) or with rastrillo (the CARLOS web framework), bringing an existing app onto it, deciding what one instance is (per person, team, customer, repo, or one shared instance; a signup that creates instances; a home or hub; reporting across tenants; SaaS customers who want on-prem), choosing a trust class (S, A1, A2, A/pub, B, C), or weighing the family's other open choices (app shape, hosting, identity). For the zero-decisions happy path to a first live app, use getting-started instead.
4---
5
6# πŸ€– Building CARLOS apps
7
8## Overview
9
10CARLOS is an application architecture: **one static Go binary, one
11fully-isolated instance per tenant (own process, own SQLite file, own unix
12socket), many instances on one small box behind a small router**, with
13every database continuously replicated to object storage and idle
14instances hibernating to it. What a tenant *is* (a person, a team, a
15customer company, a repository, a chat, or the whole app) is the app's
16first decision.
17Everything below is distilled from the prompt histories, repo instruction
18files and code of the apps it was extracted from. The house style is to
19state rules as laws, date and attribute decisions, and make rules
20mechanical rather than remembered.
21
22## When to use
23
24- Starting a new product that should join the family, especially when it
25 needs real decisions: tenancy, trust class, app shape, hosting, identity.
26- Working out what one instance of an app should be, how people reach it,
27 what the host can see, and which CARLOS benefits that earns.
28- Porting an existing app onto the model (Tito's path β€” see "Adopting").
29- Any question of the form "what would a CARLOS app do here?" β€” stack,
30 hosting, testing, deploys, process, and the trade-offs between the
31 family's sanctioned options.
32
33Not for: contributing to one of the existing apps (read that repo's
34AGENTS.md instead β€” it always wins over this skill). And for "just get me
35a first app live, decide everything for me", use the sibling skill
36**carlos:getting-started** β€” it is this skill with every decision
37pre-answered with the defaults (one instance, class C declared, server
38shape, Carloku).
39
40## Start here: tenancy, then trust
41
42Before stack, screens or deploys, answer three questions and write the
43answers down as a **tenancy declaration** in AGENTS.md. Everything else
44in this skill, and every benefit on carlosframework.com, hangs off them.
45
461. **What is one instance?** A person (Kass), a chat (Eleven), a mailbox
47 (Keymail), a team (Oficina, an internal finance ledger), a customer company (Tito), a
48 community (Woodstar), a repository (amadan), a bank connection,
49 the whole app (Movement), or nothing at all (a static site). The
50 boundary is the app's choice; there is no default. Choose the smallest
51 unit that owns data together, fails together and leaves together.
52 Sharing is membership in someone's instance, never a copy. Never
53 tenancy by a `tenant_id` column in one shared file. Totals across
54 tenants come from facts the centre owns or witnesses and fixed-shape
55 records instances push, never from opening tenant databases.
562. **How do people reach it?** One instance with in-app accounts, a
57 simple home that lists your instances, a hub that puts every tenant
58 under one domain, or a blind home for end-to-end encrypted apps.
59 Central apps are stateless: records in object storage, no database.
603. **What can the host see?** A trust class per workload: S (no user
61 data), A1/A2/A-pub (host-blind, with declared residue or public
62 surfaces), B (sealed at rest, readable while awake), C (plaintext,
63 chosen and written down).
64
65Then name the benefits the design earns, each with its mechanism (isolated
66data per tenant, blast radius of one, hibernation, per-tenant export and
67erasure, residency once switched on, a falsifiable security claim, self-hosting from the same
68binary), and the costs it accepts. **[references/tenancy.md](references/tenancy.md)**
69has the gallery's real choices, the four user models, the classes, the
70benefits table with what is live today, worked examples, and the
71declaration template. Read it first.
72
73## Where this sits now (2026-10-15)
74
75The family's conventions have progressively become running infrastructure.
76Read this skill alongside three facts:
77
78- **The platform** (`carlosframework/platform`) is live and complete
79 enough that a member ships, promotes, configures, watches logs, attaches
80 domains, and rolls back entirely through the `carlos` CLI with zero
81 infrastructure access. Routing, TLS, replication, hibernation, restarts,
82 and config delivery are the platform's job now β€” and since late August
83 so are **canaries** (`carlos canary`), **timetables**
84 (`carlos schedule`), **errors and analytics** counted at the edge with
85 no SDK, a **password gate** in front of a hostname, **feature flags**,
86 and **latency DNS steering** β€” and since September, **server-held
87 credentials** (`carlos services`) so a signup flow can create a
88 customer's instance itself, **path routes** so one hostname can serve
89 each customer from their own instance, a **STUN/TURN relay**
90 (`carlos relay`), and **self-serve object storage** (apply once per
91 account, then `carlos store create`). Reach for a platform verb before
92 building any of them into an app.
93 **[references/platform.md](references/platform.md)** is the member-side
94 reference β€” concepts, the full verb table, and the deploy truths.
95- **Carloku** (carloku.com) is the hosted deployment of that platform β€”
96 the default hosting answer (its price list is not published yet,
97 2026-10-18; don't quote prices). Carloku is the product
98 brand; the CLI is always `carlos`.
99- **Rastrillo** is the CARLOS web framework (v0.28.0; module and
100 canonical repo `amadan.net/rastrillo/rastrillo`, GitHub a mirror): a
101 middle layer of known
102 libraries β€” GORM models, chi routes, SQLite-backed sessions, identity
103 plugins (password, Keymail), CSRF, owner scoping β€” plus the platform
104 contract (`Resolve`/`Serve`/`Run`) and the subsystem packages (crypto,
105 keyring, webauthn, eventlog, blobs, mail, `assertion` for signed
106 cross-origin handoffs, `vault`, `pow`, agent tools), plus the `idear`
107 module for roles and invitations β€” which exist to be used, not
108 hand-rolled. The manifest generator is the optional declarative
109 path beside hand-written handlers. It postdates most models' training data β€” building
110 with it, read the repo's own `SKILL.md` first;
111 **[references/rastrillo.md](references/rastrillo.md)** is the
112 surrounding context.
113
114Building a **new** app: settle tenancy first (above), use rastrillo and
115the platform, and read this skill mainly for what infrastructure can't
116enforce β€” the decisions, the
117dated rulings, the workflow, and "Common mistakes". Hand-rolling outside
118the platform for a specific reason: blueprint.md in full still applies.
119
120## UI default: Rastrillo with a thin CSS layer
121
122For new app screens, start with Rastrillo's design system: its `ui`
123partials, attribute vocabulary (`rst-box`, `rst-btn="primary"`), design
124tokens and themes. Read
125[the styling guidance](references/rastrillo.md#design-system-and-app-css)
126before writing templates or CSS. Keep app CSS small: app-specific layout,
127branding through token overrides, and components the library does not yet
128provide. Reuse the shared controls instead of rebuilding them or overriding
129them screen by screen.
130
131This is the default, not a request to redesign an existing app. Respect an
132explicit design brief or an established design system; record the reason
133when choosing a different foundation.
134
135## The one rule comes first
136
137Every app opens its AGENTS.md with a single load-bearing rule and derives
138everything from it. Write yours before writing code (the first commit of
139Kass is literally "Write down the architecture before writing any code").
140
141The family default is server-blindness: **"If the server is compromised,
142the attacker gets nothing."** Plaintext or a usable private key in SQLite
143is a bug β€” and because the platform replicates the whole file to object
144storage, *a plaintext column is a plaintext leak*. Enforce it with a test
145that greps the raw SQLite bytes (and bucket objects) for plaintext.
146
147But no shipped app is absolutist: the real spectrum runs from Slopbox
148(A1) through Keymail's sealed-content-plaintext-envelopes (A2) and
149Woodstar's split-by-content-class (A2 + A/pub) to an internal finance ledger's sealed
150single-tenant (B) and Tito's deliberate server trust (C), and the
151discipline that makes any position legitimate is the same β€” **choose on
152day one, name every leak, publish the list**. The whole axis, with each app's recorded
153reasoning and a decision guide, is
154**[references/decisions.md](references/decisions.md)** β€” read it before
155settling a new app's trust model, app shape, hosting, or identity story.
156
157## Guiding principles
158
1591. **Intent before code.** AGENTS.md is written first and is binding. Design
160 docs precede big builds; the doc's decisions are taken β€” if one proves
161 wrong, stop and flag it in the PR rather than redesigning.
1622. **Decisions are dated, attributed, and settled.** "Paul's call,
163 2026-07-26" next to every ruling, rejected alternatives recorded beside
164 it, under a heading like "Settled decisions (don't relitigate casually)".
1653. **Built to outlive AI.** Human legibility beats AI convenience. Never add
166 machinery only an LLM can drive; operational knowledge lives in scripts
167 and docs in the repo, never solely in prompts or agent memory. "No LLM
168 cookbooks": ops run through a command, never reconstructed from prose β€”
169 a script drifts loudly, a document drifts silently.
1704. **Rules are mechanical, not remembered.** The module-size cap is a test.
171 Contrast ratios are computed from the token file by a test. A guard a
172 shell step can satisfy is a guard a shell step can be wrong about β€” put
173 guards inside the test binary. Generated configs are marked GENERATED
174 and regenerated on a timer, never hand-edited.
1755. **Blast radius is the unit of design.** One process + one DB per account:
176 a panic takes down one person. The corollary cuts both ways: strict
177 validation belongs where its blast radius is one request, not one fleet
178 (a shared package that panics at init once 502'd every process at once).
1796. **Defer until earned, with the trigger written down.** Keep a
180 "Deliberately not built yet" section. And ship a thing *off* rather
181 than untested when the only real proof needs infrastructure you don't
182 have yet.
1837. **Don't diverge gratuitously from the family.** Shared shape is what
184 makes components extractable; a local improvement to shared code is a
185 debt to upstream, noted as such.
1868. **Honesty over polish.** Publish trade-offs plainly ("Honest trade-offs
187 β€” read before trusting it"), never overclaim maturity, and don't let
188 the UI lie: an uncertain state is a sentence, never a fabricated number,
189 and a claim about a person ("you earned your rest") has to be true.
1909. **Every deviation from the one rule is enumerated**, justified, and
191 published β€” never hidden.
192
193## The eleven factors
194
195CARLOS is the architecture; the values under it are **the eleven factors**
196([11factor.org](https://11factor.org) is the canonical text β€” link to it,
197don't copy it). Sibling repos cite factors **by number**, so know the names:
198
199I. Trust no one Β· II. Let kids play Β· III. Encrypt everything Β·
200IV. Great design is for everyone Β· V. Intent is the system Β·
201VI. Built by humanity, owned by humanity Β· VII. Self-hosting is a right Β·
202VIII. Many small things Β· IX. Inefficient builds efficient Β·
203X. Humans come first Β· XI. Centralised infrastructure is glue.
204
205The ones the family invokes operationally:
206
207- **III β€” Encrypt everything** β†’ the one rule: server-blind by default;
208 deviations are legitimate under the deviation rule, not violations.
209- **V β€” Intent is the system** β†’ AGENTS.md and design docs before code;
210 "the code should have to argue with something."
211- **VI β€” Built by humanity, owned by humanity** β†’ open source; private
212 until it works is allowed, but "factor VI is not optional, only
213 deferred."
214- **VII β€” Self-hosting is a right** β†’ self-hosting is first-class, never a
215 degraded tier; hosted convenience (Carloku) is the product, never the
216 software.
217- **VIII β€” Many small things** β†’ instance-per-account, scale by adding
218 instances, not replicas of one.
219- **X β€” Humans come first** β†’ AI is "a guest with a name tag": opt-in,
220 never required, always disclosed (πŸ€– marking), and every path works with
221 the AI switched off. The strongest form is Kass's: the AI is an ordinary
222 member whose code never runs inside the server process, and whose
223 conclusions land as proposals into human review, never as direct writes.
224
225## Quick reference β€” the shape
226
227| Concern | The CARLOS answer |
228|---|---|
229| Language | Go, one static binary, `CGO_ENABLED=0`, thin `main.go` dispatch |
230| Dependencies | Stdlib first; "adding a dependency is a decision, not a default"; hand-roll small clients over SDKs; a scoped exception is dated and does not extend to its neighbours |
231| Storage | `modernc.org/sqlite`, WAL, one DB per instance, additive-only migrations β€” automatic on rastrillo |
232| Quantities | Integer cents, integer grams β€” "a float never touches an amount" |
233| Frontend | Two shapes, chosen deliberately: `server` (server-rendered, zero-JS baseline β€” the default) or `client` (Woodstar's shape) β€” see decisions.md Β§2 |
234| JS discipline | 300-line module cap enforced by test, ratchet-down only; VanJS (vendored) the one sanctioned reactive dependency |
235| Routing, TLS, replication, hibernation, restarts | The platform's job β€” see platform.md; hand-rolled only off-platform (blueprint.md) |
236| Periodic work | `carlos schedule` β€” a time and a path; each fire is a POST to a route the app already serves, over its own socket. Never cron, never a second worker. A schedule tighter than the idle window stops the instance hibernating, and that is billed β€” platform.md |
237| Object storage | Once per account, `carlos store apply` and a deployment operator approves; then `carlos store create` declares a bucket and credentials arrive as env; `carlos store scan` is opt-in virus scanning, off by default. It reconciles β€” it counts the objects it has NOT looked at β€” so ask for both numbers. Server-blindness wins over it: a client-encrypted store gets no coverage claim, because nothing can read the bytes β€” platform.md |
238| Errors and analytics | `carlos errors` needs no wiring and collects from day one (tagged log lines; edge-seen 5xx are `carlos availability`); `carlos analytics` counts at the edge with no JavaScript, no cookie, no stored IP, and is opt-in per app or account. Do not add a third-party analytics script to a CARLOS app β€” platform.md |
239| Canaries | `carlos canary` β€” a branch-only channel and hostname, outside every pipeline, seven-day lease. Review there, never on localhost. Pass `--environment` if the app carries config, or every form submission on the canary host fails β€” platform.md |
240| Config environments | `carlos env --environment` writes a named bundle; a route is bound to one via the instance record (`instances set-environment`), then `carlos restart --environment` cycles exactly what reads it β€” platform.md |
241| Feature flags | `carlos features set` β€” the platform serves them, only the app interprets them, and they land on the instance's next poll with no restart β€” platform.md |
242| Hostname-level password | `carlos gate` β€” one shared password at the edge in front of an app's hostnames, for a prototype or a private static site. A curtain over a hostname, never a substitute for the app's own accounts β€” platform.md |
243| Outbound email | `carlos email enable` β€” the platform mints the sending identity, publishes DKIM/SPF/DMARC, delivers SMTP credentials as env; never run an MTA or hold a cloud mail key β€” platform.md |
244| Scheduled work | `carlos schedule set` declares it; the app's part is a POST handler guarded by `carlos.Tick` β€” the instance is asleep, so never an in-process cron β€” platform.md |
245| Tenancy | The app chooses what one instance is; the declaration (unit, user model, class per workload, residue, benefits, costs) goes in AGENTS.md before code β€” tenancy.md |
246| A customer per instance | The signup app holds an `instance`-role credential (`carlos services create --role instance --app <tenant-app>`) and `POST`s `/api/cli/apps/<account>/<tenant-app>/instances` β€” customers are instances, never Carloku accounts. The credential is app-wide, so it lives on the signup app only β€” platform.md "Provisioning instances from your own app" |
247| One hostname, many instances | Path routes: the first path segment of a claimed hostname points at another route in the same account, on the same box. One hostname is one origin β€” platform.md "One hostname, many instances" |
248| Central apps (sign-in, directory, home) | Stateless: no SQLite of their own, records in object storage, so they run anywhere and lose nothing when moved |
249| WebRTC | `carlos relay enable` β€” the platform's STUN/TURN, delivered as `CARLOS_RELAY`; never a third-party TURN service β€” platform.md |
250| Hosting | Carloku hosted (default) / customer fleets / self-hosted platform β€” decisions.md Β§3 |
251| Deploys | `carlos deploy`: ship + promote + watch `X-Carlos-Version` until live; verify against the thing you changed with the binary you built. A static site is the same one command β€” `--kind static --host <h> <dir>`, which declares the route too β€” platform.md |
252| Activity descriptions | Pass a concise `--message` on supported mutations: say what changes and why. The console records the exact target separately. Agents supply the message even when prompts are disabled β€” platform.md |
253| Identity | "Sign in with Keymail" / passkeys+PRF / magic link+TOTP by trust model β€” decisions.md Β§4 |
254| Trust | A class per workload, S to C; server-blind by default; every softening named and grep-tested. Placement by class is designed, not enforced β€” tenancy.md Β§3, decisions.md Β§1 |
255| UI stance | Hide the machinery: no hostnames, keys, or crypto vocabulary in the default flow β€” "no nerdspeak"; calm UI, red for danger only, no toasts |
256| Process | Code on amadan by default (its agent house rules: `amadan agent skill -install`); worktree per session, branch, regular pushed commits; merge to main (`amadan branch merge`, optionally `-squash`) only when done, approved and CI green (CI green binds once the repo has CI β€” a new repo's first merges land on done + approved and add CI early); review on a deployed canary, never localhost |
257| Authorship | πŸ€–/πŸ‘¨ markers, `Co-Authored-By: Claude …` trailers, published prompt + carbon ledgers |
258
259## Details
260
261- **[references/tenancy.md](references/tenancy.md)** β€” read first: what
262 one instance is, across every shape in the gallery; the four user
263 models (one instance, simple home, hub, blind home); stateless central
264 apps; the trust classes S to C; the benefits each choice earns, with
265 mechanism and what is live; and the tenancy declaration to write
266 before code.
267- **[references/decisions.md](references/decisions.md)** β€” the four axes:
268 trust model, app shape, hosting, identity β€” each with the family's real
269 positions, recorded reasons, and a decision guide.
270- **[references/platform.md](references/platform.md)** β€” the platform
271 member-side: concepts (accounts, releases, channels, instances,
272 hibernation, config bundles, canaries), the `carlos` CLI verb table,
273 deploying a static site, standing a canary beside production, scheduled
274 work (the tick contract, idempotency, one-shot timers, what a tight
275 schedule costs), errors and analytics, sending email (SMTP credentials,
276 DKIM/DMARC alignment, the credential-propagation trap), deploy truths,
277 self-hosting.
278- **[references/rastrillo.md](references/rastrillo.md)** β€” building an
279 app with rastrillo: the middle-layer shape, where the authoritative
280 SKILL.md lives, the safety rules, and the declarative manifest path.
281- **[references/blueprint.md](references/blueprint.md)** β€” the technical
282 blueprint the platform automated: the carlos core, storage rules,
283 crypto conventions, box setup. The reference for self-hosting outside
284 the platform and for understanding what it does on your behalf.
285- **[references/process.md](references/process.md)** β€” working
286 conventions: repo docs, git/PR workflow, canaries, the test layers
287 (including the uidump pattern), `/end-session`, ledgers and
288 AI-authorship marking.
289
290## Adopting CARLOS in an existing app (Tito's path)
291
2921. Restate the model in your first commit and at the top of README and
293 AGENTS.md.
2942. Deploy onto the platform (hosted or self-hosted) rather than porting
295 the router/replication machinery β€” that is what it is for. Only when
296 the app must run somewhere the platform can't: take the core into
297 `internal/carlos`, kept close to blueprint.md's shape so it can be
298 swapped for the platform later.
2993. Adopt the conventions alongside (worktrees, PR playbook, canaries,
300 additive migrations, zero-JS-first).
3014. List every deferred component with its trigger; list every deliberate
302 departure with its reason.
3035. Fixes you make to shared-shape code are candidates to upstream β€” record
304 them.
305
306## Common mistakes
307
308| Mistake | Reality |
309|---|---|
310| One shared instance with a `tenant_id` column for many customers | Isolation on CARLOS is a process and a file. A `WHERE` clause gives up per-tenant isolation, erasure, residency and hibernation, and one missed clause leaks everyone. Make the tenant an instance β€” tenancy.md |
311| Starting to code before deciding what one instance is | The unit decides the user model, the trust class's blast radius and which benefits the app can claim. Retrofitting it is a migration across every instance. |
312| Adding a framework/bundler "just for this screen" | The no-build-step rule is load-bearing (auditability, longevity). One more ES module, one concern. |
313| Choosing the client shape because it feels modern | The client shape is for client-held keys, live channels, and server-blind state β€” and it must self-impose the discipline the server shape gets free (decisions.md Β§2). |
314| Creating a Carloku account per customer, or signing up customers with a person's CLI login | Accounts are *your* tenancy on the platform; customers are instances of your app. A signup flow holds a service credential (`carlos services`) and calls the instances API β€” platform.md |
315| Giving every tenant its own hostname because "the edge routes by Host only" | It no longer does: path routes split one claimed hostname by its first segment, within one account and one box β€” platform.md |
316| Hand-rolling a router, certs, Litestream, or restart machinery | The platform's job. An app on the platform is a binary on a unix socket (`--socket`/`--db`; there is no `$PORT`). |
317| Giving a static site an instance | It has no process to run. The edge serves it off the channel pointer, so `carlos deploy --kind static --host <h> <dir>` is the whole deploy β€” `instances enable`/`create` is the binary recipe, and following it here builds a route that succeeds at every step and can never wake. |
318| Concluding you have no access because the console redirected you | A `curl` of the console 302s to a login page for everyone β€” a browser session is not what the CLI holds. `carlos auth whoami` is the only thing that answers the question, and it is the first command of any platform task. |
319| Guessing a channel name from the ladder in `--help` | Channels belong to the app. A new app has exactly one (`edge`), and `canary β†’ edge β†’ beta β†’ stable` is the frozen legacy ladder for apps that declare no pipeline. `carlos channels` lists this app's; `carlos pipeline` orders them. |
320| Growing the biggest file/package | A new concern gets a new small module or package β€” never more growth of the biggest one. |
321| Destructive migration "to clean up" | Migrations are additive-only. New code over an old DB must always be safe. Never delete data to update. |
322| Trusting a backup that exists | "A backup you've never restored is a hope, not a backup." (Platform-run for platform apps; your job off-platform.) |
323| Verifying a deploy with a 200 | An app-shell route 200s for any build ever shipped. Only `X-Carlos-Version` on the canonical host, after the watch, is proof β€” and minted alias hosts carry no version header by design. |
324| Treating a promote as a deploy | Until the process cycles, the old binary serves. Platform-known units cycle automatically; hibernating tenants wake into the new build; a bespoke unit stays old until something restarts it. |
325| Reviewing on localhost | Review happens on a deployed canary, always. Shared hosts only ever run merged main. |
326| Skipping the post-deploy browser check | A JS syntax error takes the whole client down and only a real engine sees it (the keymail rule). |
327| Treating a day-one absolute as unsatisfiable and quietly skipping it | A gate a brand-new project cannot meet is a gate not written for day one. Name the bootstrap case in the rule and make satisfying it early work β€” a rule agents learn to step around stops working everywhere. |
328| Relitigating a settled decision in a drive-by | Settled decisions change with new facts, dated and attributed β€” not casually. |
329| A retired stack left owning DNS or deploy paths | It can silently undo a cutover (a legacy apply once reverted a live A record). Move the records; delete the verb that can report success while changing nothing. |
330