| 1 | # π€ The decisions β trust, shape, hosting, identity |
| 2 | |
| 3 | The four axes every CARLOS app settles on day one, after tenancy (what |
| 4 | one instance is and how people reach it: tenancy.md, which comes first |
| 5 | because it decides the blast radius of every answer here). Each section gives the |
| 6 | family's real positions with the reasons their own docs record β offer |
| 7 | these as choices with trade-offs, not as one right answer. Whatever is |
| 8 | chosen: **date it, attribute it, record the rejected alternative, and |
| 9 | publish every deviation.** Retrofitting any of these is a migration, not |
| 10 | an edit. |
| 11 | |
| 12 | ## 1. Trust model β what can the server read? |
| 13 | |
| 14 | The family default is server-blindness ("if the server is compromised, |
| 15 | the attacker gets nothing"), but no shipped app is absolutist. The real |
| 16 | spectrum is defined by **which leaks you name**: every app publishes its |
| 17 | softenings as a list, and enforces the boundary with tests that grep raw |
| 18 | SQLite bytes and bucket objects for plaintext. A "partial" model is |
| 19 | legitimate exactly when the leak is named; it is a violation when hidden. |
| 20 | |
| 21 | **The positions, with their recorded reasons:** |
| 22 | |
| 23 | Each position below carries its trust class (S, A1, A2, A/pub, B, C; |
| 24 | tenancy.md Β§3), declared per workload. Slopbox is the A1 case: padded, |
| 25 | fixed-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 |
| 72 | parties and the product promise is "we can't read it" β seal it and eat |
| 73 | the costs (no server search, recovery becomes product surface, key loss |
| 74 | is data loss by design). Public or bridged to a public network β sealing |
| 75 | it is theater; spend the effort on the private classes beside it. A |
| 76 | support team that must read it β server trust, argued in writing from |
| 77 | support load, repaid with origin splits and authorization design. An |
| 78 | agent/automation that must act on it unattended β sealed single-tenant, |
| 79 | with the residual risk in the README. |
| 80 | |
| 81 | **Whatever you choose:** the merciless grep test from day one, the |
| 82 | deviations list in the README, and β server-blind apps β remember the |
| 83 | bucket: Litestream ships the whole SQLite file to object storage, so a |
| 84 | plaintext column is a plaintext leak. The envelope mechanics for sealed |
| 85 | content (HKDF purpose keys, ECDH β AES-GCM, ECIES-wrapped per-thread/ |
| 86 | content keys β which is also how a sealed object gains a second named |
| 87 | reader) are blueprint.md's crypto section; golden vectors pin every |
| 88 | implementation. **Honesty note (2026-08-23): rastrillo does not yet |
| 89 | properly support the E2EE use-case as an app story** β its crypto and |
| 90 | webauthn packages are primitives, and framework-level support is being |
| 91 | worked on now. A server-blind app today hand-builds the app-side flow |
| 92 | on the Keymail/Kass/Woodstar patterns; don't claim the framework does |
| 93 | it. |
| 94 | |
| 95 | ## 2. App shape β server-rendered or client-owned? |
| 96 | |
| 97 | The family recognizes exactly two shapes; the axis is written into the |
| 98 | framework's design (chosen at scaffold time in the rastrillo ui design; |
| 99 | the `server` shape is what `rastrillo new` ships today, the `client` |
| 100 | shape is hand-built on the Woodstar/Eleven pattern until the framework |
| 101 | grows 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 |
| 124 | JS owns the paint. The family's evidence for why: Eleven's hand-rolled |
| 125 | shell grew to 8,722 lines with one render function invoked from 31 call |
| 126 | sites β the bug class a reactive binding removes structurally, and the |
| 127 | reason VanJS is the sanctioned default (framework-free remains |
| 128 | legitimate; anything heavier than VanJS is a redesign). Woodstar keeps |
| 129 | the 300-line module cap test-enforced (one coordinator module may carry |
| 130 | a higher cap; caps only ratchet down), builds all DOM through a |
| 131 | falsy-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 |
| 133 | shows the page assembling, and runs three browser drives in CI because |
| 134 | a JS syntax error takes the whole client down and only a real engine |
| 135 | sees it. |
| 136 | |
| 137 | **Platform cost differs too.** A server-shape app is one route. A |
| 138 | client-shape product like Woodstar is several cooperating binaries with |
| 139 | in-app path fan-out (one proxy in front of its binaries; the edge's path |
| 140 | routes split a hostname only by its first segment, and only onto whole |
| 141 | routes β platform.md), WebSocket upgrades |
| 142 | and SSE proxied through two hops (verify the 101 through the real edge, |
| 143 | never assume it), wildcard customer domains via ACME delegation, and a |
| 144 | held socket that interacts with hibernation. All supported β none free. |
| 145 | |
| 146 | **Choosing:** default to `server`. Move to `client` only for a product |
| 147 | that genuinely needs client-side crypto, live channels, or server-blind |
| 148 | reader state β not because a richer client feels more modern. The zero-JS |
| 149 | baseline binds server-shape surfaces; a screen that cannot honor its |
| 150 | promise without JS (sealing in the browser) *is* a client-shape surface, |
| 151 | and the two compose within one product: Woodstar's status page is a |
| 152 | plain static app beside the SPA, and a sealed app's marketing and |
| 153 | settings 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 | |
| 190 | Instance-per-account makes single sign-in a day-one problem (three |
| 191 | instances must not mean three credentials). The family's current |
| 192 | positions: |
| 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 |
| 218 | should reach for `rastrillo/auth` before inventing anything β the |
| 219 | family default: magic-link email that auto-upgrades to "Sign in with |
| 220 | Keymail" when the address has a claimed inbox, so every address works. |
| 221 | `rastrillo/password` is the classic email+password alternative, and |
| 222 | step-up (`sessions.RequireFresh`) is the hardening seam. |
| 223 | Server-blind apps: passkeys + PRF, and treat recovery as product |
| 224 | surface. Server-trust apps: magic link + TOTP. Passkey RPs pin to a |
| 225 | hostname β pick the credential origin deliberately and early (renaming |
| 226 | an RP later is a multi-phase drill), and never share it with an origin |
| 227 | that carries third-party or account-authored content. |
| 228 | |