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# πŸ€– The decisions β€” trust, shape, hosting, identity
2
3The four axes every CARLOS app settles on day one, after tenancy (what
4one instance is and how people reach it: tenancy.md, which comes first
5because it decides the blast radius of every answer here). Each section gives the
6family's real positions with the reasons their own docs record β€” offer
7these as choices with trade-offs, not as one right answer. Whatever is
8chosen: **date it, attribute it, record the rejected alternative, and
9publish every deviation.** Retrofitting any of these is a migration, not
10an edit.
11
12## 1. Trust model β€” what can the server read?
13
14The family default is server-blindness ("if the server is compromised,
15the attacker gets nothing"), but no shipped app is absolutist. The real
16spectrum is defined by **which leaks you name**: every app publishes its
17softenings as a list, and enforces the boundary with tests that grep raw
18SQLite bytes and bucket objects for plaintext. A "partial" model is
19legitimate exactly when the leak is named; it is a violation when hidden.
20
21**The positions, with their recorded reasons:**
22
23Each position below carries its trust class (S, A1, A2, A/pub, B, C;
24tenancy.md Β§3), declared per workload. Slopbox is the A1 case: padded,
25fixed-size blobs and encrypted names, so only transport shape leaks.
26
27- **E2EE content, plaintext routing metadata β€” Keymail (A2).** Bodies and
28 subjects are ciphertext; from/to/date/folder are readable, published
29 as a trade-off ("like SMTP envelopes"). Its named softenings show what
30 honest partial looks like: inbound ordinary email is plaintext
31 *transiently in RAM* at the boundary, then sealed server-side to the
32 mailbox's public key (sealing needs only the public half); opt-in
33 imports state before running that messages pass through memory; mixed
34 sends tell the user what travels unsealed. And a trade *refused*:
35 searchable encryption rejected β€” the client legitimately holds
36 plaintext and keys, so the index is client-side and the server learns
37 nothing new.
38- **E2EE content, blind identity, public reference data β€” Kass (A2;
39 its agent sidecar is C).** A
40 training log is medical-adjacent, so user content is sealed β€” but the
41 exercise database is public reference data embedded in the binary,
42 deliberately unencrypted, and the identity registry stores email only
43 as an HMAC blind index under a pepper (the readable address lives
44 solely in unclaimed sign-in links, blanked on claim). The bar its docs
45 set: *the schema could be published as the privacy policy* β€” leak the
46 whole registry and the worst anyone learns is roughly how many people
47 signed up, and when.
48- **Split by content class β€” Woodstar (A2 + A/pub).** Public posts are plaintext on
49 the server (they are public; encrypting them would be theater), DMs
50 are sealed end-to-end, and private state β€” follows, petnames, drafts,
51 settings β€” is server-blind encrypted blobs, so the server never learns
52 the social graph. Federation (ActivityPub/Bluesky) is an explicit,
53 bounded, opt-in leak of the public class only; DM bridging is a
54 permanent non-goal.
55- **Sealed single-tenant β€” an internal finance ledger (B).** Data encrypted
56 at rest, but the instance holds the key in memory because unattended
57 bank syncs need plaintext with no human present. The README names the
58 residual risk out loud ("a compromised live host can read data") and
59 the copy never claims otherwise; the plaintext-grep test still runs,
60 with one dated, allowlisted exception. The shape for tools that must
61 act on the data autonomously.
62- **Server-side trust β€” Tito (C).** A ticketing platform holds buyer PII in
63 plaintext already; there is no encrypted-blob requirement to inherit,
64 and a six-person support team must be able to read an email address
65 (Vicky's rule: every decision is weighed by the support load it
66 creates). What compensates is **authorization, not decryption** β€”
67 credentials only ever on the root origin, account-authored content
68 only on the account's own host, and operator escape hatches (2FA
69 reset) that open doors without unlocking data.
70
71**Choosing:** classify the content, not the app. Private between named
72parties and the product promise is "we can't read it" β†’ seal it and eat
73the costs (no server search, recovery becomes product surface, key loss
74is data loss by design). Public or bridged to a public network β†’ sealing
75it is theater; spend the effort on the private classes beside it. A
76support team that must read it β†’ server trust, argued in writing from
77support load, repaid with origin splits and authorization design. An
78agent/automation that must act on it unattended β†’ sealed single-tenant,
79with the residual risk in the README.
80
81**Whatever you choose:** the merciless grep test from day one, the
82deviations list in the README, and β€” server-blind apps β€” remember the
83bucket: Litestream ships the whole SQLite file to object storage, so a
84plaintext column is a plaintext leak. The envelope mechanics for sealed
85content (HKDF purpose keys, ECDH β†’ AES-GCM, ECIES-wrapped per-thread/
86content keys β€” which is also how a sealed object gains a second named
87reader) are blueprint.md's crypto section; golden vectors pin every
88implementation. **Honesty note (2026-08-23): rastrillo does not yet
89properly support the E2EE use-case as an app story** β€” its crypto and
90webauthn packages are primitives, and framework-level support is being
91worked on now. A server-blind app today hand-builds the app-side flow
92on the Keymail/Kass/Woodstar patterns; don't claim the framework does
93it.
94
95## 2. App shape β€” server-rendered or client-owned?
96
97The family recognizes exactly two shapes; the axis is written into the
98framework's design (chosen at scaffold time in the rastrillo ui design;
99the `server` shape is what `rastrillo new` ships today, the `client`
100shape is hand-built on the Woodstar/Eleven pattern until the framework
101grows it).
102
103- **`server` (the default): server-rendered HTML, zero-JS baseline as a
104 non-negotiable.** Every screen and primary action works with scripts
105 stripped; JS is enhancement only, enforceable by a test (fetch the
106 page script-stripped, diff for every affordance) rather than a
107 browser. Interactive components are real HTML first (a dropdown is
108 `<details>/<summary>`). This is Tito, the platform console,
109 and every admin/CRUD/dashboard surface in the family.
110 It is also what hibernation likes: request/response apps sleep well.
111- **`client`: the app runs in the browser and owns its state.** Chosen
112 when the product needs what server rendering cannot do: client-held
113 keys and client-side crypto (sealing DMs, encrypting blobs, proof-of-
114 work β€” work that must happen where the seed is), live feeds over
115 WebSocket/SSE, and reader-owned state the server never learns.
116 This is Woodstar: a hand-written no-build-step ES-module SPA, VanJS
117 (140 vendored lines) as a thin reactive shell β€” views return
118 `{ el, update }` β€” with local caches merged against server-blind
119 blobs for multi-device continuity. Keymail and Kass are client-shape
120 on their sealed surfaces for the same reason: E2EE content forces the
121 shape, because sealing must happen where the keys are.
122
123**The client shape's price is discipline it must self-impose**, because
124JS owns the paint. The family's evidence for why: Eleven's hand-rolled
125shell grew to 8,722 lines with one render function invoked from 31 call
126sites β€” the bug class a reactive binding removes structurally, and the
127reason VanJS is the sanctioned default (framework-free remains
128legitimate; anything heavier than VanJS is a redesign). Woodstar keeps
129the 300-line module cap test-enforced (one coordinator module may carry
130a higher cap; caps only ratchet down), builds all DOM through a
131falsy-dropping helper (a stray `null` child renders as the literal text
132"null" β€” a real shipped bug), gates first paint so a cold load never
133shows the page assembling, and runs three browser drives in CI because
134a JS syntax error takes the whole client down and only a real engine
135sees it.
136
137**Platform cost differs too.** A server-shape app is one route. A
138client-shape product like Woodstar is several cooperating binaries with
139in-app path fan-out (one proxy in front of its binaries; the edge's path
140routes split a hostname only by its first segment, and only onto whole
141routes β€” platform.md), WebSocket upgrades
142and SSE proxied through two hops (verify the 101 through the real edge,
143never assume it), wildcard customer domains via ACME delegation, and a
144held socket that interacts with hibernation. All supported β€” none free.
145
146**Choosing:** default to `server`. Move to `client` only for a product
147that genuinely needs client-side crypto, live channels, or server-blind
148reader state β€” not because a richer client feels more modern. The zero-JS
149baseline binds server-shape surfaces; a screen that cannot honor its
150promise without JS (sealing in the browser) *is* a client-shape surface,
151and the two compose within one product: Woodstar's status page is a
152plain static app beside the SPA, and a sealed app's marketing and
153settings pages can stay server-shape beside its client-shape editor.
154
155## 3. Hosting β€” Carloku, your boxes, or your platform
156
157- **Carloku (hosted) β€” the default.** Zero infra: the getting-started
158 path. The pricing design is per app (baseline + instance-hours;
159 hibernating apps cost ~the baseline; egress/storage metered
160 separately; free rungs), but no price list is published yet
161 (2026-10-18): don't quote prices or promise a free tier. Nothing about
162 the other axes locks you out of moving later.
163- **Vendors: one codebase, hosted and customer-run.** With one instance
164 per customer, an on-prem request stops being a fork: the customer's
165 instance runs on their boxes and bucket (a customer fleet) or their own
166 platform, following your release channel. Tito proves the self-hosted
167 half in three regions; no vendor has yet shipped to customers this way
168 (Jelly is in development). Publish a licence class (L1 to L5,
169 tenancy.md Β§3) so customers know what they may run.
170- **Customer fleets β€” your boxes, the hosted control plane.** `carlos
171 fleets` attaches boxes you own (a Pi, an office server, edge devices)
172 to your account; apps place onto them with `carlos apps place`. Pay
173 per box, apps free β€” the family rule is *you pay per app or per box,
174 never both*. The path for hardware you must own without operating a
175 whole platform.
176- **Self-hosted platform β€” Tito's path.** Run the platform software
177 yourself: own cloud account, own console, own boxes; your members use
178 the same `carlos` CLI against your console. Choose it when you must be
179 the operator of record (residency, regulation, or the trust model
180 itself). The cost is real operations: provisioning, platform rolls,
181 the adopt/restart cadence, and being the one who debugs below the API.
182- **Hand-rolled (no platform) β€” the historical path, now a deliberate
183 exception.** blueprint.md remains the complete recipe (it is what the
184 platform automated), for the app that must run somewhere the platform
185 doesn't. Keep the core close to the blueprint's shape so it can be
186 swapped for the platform later.
187
188## 4. Identity β€” how people sign in
189
190Instance-per-account makes single sign-in a day-one problem (three
191instances must not mean three credentials). The family's current
192positions:
193
194- **"Sign in with Keymail"** β€” Keymail has become the family's identity
195 provider: an anonymous, usernameless passkey vault (the home-vault
196 pattern, ported from Eleven's design β€” content-blind, one padded
197 sealed blob per person, the vault *is* the instance list so the
198 server can't count your instances). It now also speaks OAuth for
199 third-party apps: the `client_id` is the app's origin (no registration
200 queue β€” DNS+TLS already establish what a registry would), PKCE, one
201 grant per origin, and an app token authorizes delivery and identity,
202 never reading. The hosted consoles authenticate this way, and
203 sensitive console actions use passkey step-up that deliberately never
204 mints a stronger credential than the caller had.
205- **Per-instance passkeys + PRF** (Kass): the WebAuthn PRF output wraps
206 the data seed, so possession of the passkey β€” not the server β€” is the
207 security boundary. Recovery is an explicit table of wrapped seeds:
208 losing every row is losing the data, by design and by disclosure.
209- **Magic link + mandatory TOTP** (Tito, server-trust): boring, support-
210 friendly, with hashed single-use recovery codes and an operator reset
211 verb that is safe *because* nothing it unlocks is encrypted under it.
212 Identity lives beside the router, outside the per-account blast
213 radius, and reaches instances only as **signed** assertions β€” headers
214 are stripped-and-re-minted, and signing (not stripping alone) is what
215 stops a peer socket from forging them.
216
217**Choosing:** hosted-platform apps get console sign-in for free and
218should reach for `rastrillo/auth` before inventing anything β€” the
219family default: magic-link email that auto-upgrades to "Sign in with
220Keymail" when the address has a claimed inbox, so every address works.
221`rastrillo/password` is the classic email+password alternative, and
222step-up (`sessions.RequireFresh`) is the hardening seam.
223Server-blind apps: passkeys + PRF, and treat recovery as product
224surface. Server-trust apps: magic link + TOTP. Passkey RPs pin to a
225hostname β€” pick the credential origin deliberately and early (renaming
226an RP later is a multi-phase drill), and never share it with an origin
227that carries third-party or account-authored content.
228