| 1 | <!doctype html> |
| 2 | <html lang="en"> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | <title>The CARLOS Platform</title> |
| 7 | <meta name="description" content="π€ The place where CARLOS apps run: ship an immutable release, get a canary URL in seconds, promote the same artifact to stable. Apps sleep to a bucket when idle and wake on demand. The whole platform is one binary, a bucket, and DNS β open, and trivially self-hostable."> |
| 8 | <meta property="og:title" content="The CARLOS Platform"> |
| 9 | <meta property="og:description" content="A black-box deployment platform for CARLOS apps. One binary, a bucket, and DNS β Heroku's ease, keeping CARLOS's promises."> |
| 10 | <meta property="og:type" content="website"> |
| 11 | <meta property="og:url" content="https://platform.carlosframework.com/"> |
| 12 | <style> |
| 13 | :root { |
| 14 | --bg: #ffffff; |
| 15 | --ink: #1d2029; |
| 16 | --muted: #64707f; |
| 17 | --accent: #0f5e85; |
| 18 | --accent-soft: rgba(15, 94, 133, 0.09); |
| 19 | --rule: #e7e9f0; |
| 20 | --max: 42rem; |
| 21 | --wide: 62rem; |
| 22 | } |
| 23 | @media (prefers-color-scheme: dark) { |
| 24 | :root { |
| 25 | --bg: #14151c; |
| 26 | --ink: #eef0f6; |
| 27 | --muted: #93a4ba; |
| 28 | --accent: #5db8dd; |
| 29 | --accent-soft: rgba(93, 184, 221, 0.12); |
| 30 | --rule: #2a2e38; |
| 31 | } |
| 32 | } |
| 33 | :root[data-theme="dark"] { |
| 34 | --bg: #14151c; |
| 35 | --ink: #eef0f6; |
| 36 | --muted: #93a4ba; |
| 37 | --accent: #5db8dd; |
| 38 | --accent-soft: rgba(93, 184, 221, 0.12); |
| 39 | --rule: #2a2e38; |
| 40 | } |
| 41 | :root[data-theme="light"] { |
| 42 | --bg: #ffffff; |
| 43 | --ink: #1d2029; |
| 44 | --muted: #64707f; |
| 45 | --accent: #0f5e85; |
| 46 | --accent-soft: rgba(15, 94, 133, 0.09); |
| 47 | --rule: #e7e9f0; |
| 48 | } |
| 49 | * { margin: 0; padding: 0; box-sizing: border-box; } |
| 50 | body { |
| 51 | background: var(--bg); |
| 52 | color: var(--ink); |
| 53 | font: 400 1.0625rem/1.65 Charter, Georgia, "Times New Roman", serif; |
| 54 | -webkit-font-smoothing: antialiased; |
| 55 | padding: 0 1.25rem; |
| 56 | } |
| 57 | a { color: var(--accent); text-decoration-thickness: 1px; text-underline-offset: 2px; } |
| 58 | strong { font-weight: 700; } |
| 59 | code { |
| 60 | font-family: ui-monospace, SFMono-Regular, Menlo, monospace; |
| 61 | font-size: 0.88em; background: var(--accent-soft); |
| 62 | padding: 0.1em 0.35em; border-radius: 0.25rem; |
| 63 | } |
| 64 | |
| 65 | /* hero */ |
| 66 | header { |
| 67 | max-width: var(--wide); margin: 0 auto; |
| 68 | padding: 5rem 0 3rem; text-align: center; |
| 69 | } |
| 70 | h1 { |
| 71 | font-size: 2.9rem; line-height: 1.1; letter-spacing: 0.01em; |
| 72 | margin: 0 0 0.85rem; font-weight: 700; |
| 73 | } |
| 74 | .tagline { font-size: 1.2rem; color: var(--muted); font-style: italic; } |
| 75 | .lede { |
| 76 | font-size: 1.2rem; max-width: 38rem; |
| 77 | margin: 1.5rem auto 0; |
| 78 | } |
| 79 | .cta { |
| 80 | margin: 2rem 0 0; display: flex; gap: 0.75rem; |
| 81 | justify-content: center; flex-wrap: wrap; |
| 82 | } |
| 83 | .btn { |
| 84 | font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; |
| 85 | font-size: 0.95rem; font-weight: 600; text-decoration: none; |
| 86 | padding: 0.65rem 1.25rem; border-radius: 0.5rem; |
| 87 | } |
| 88 | .btn.primary { background: var(--accent); color: var(--bg); } |
| 89 | .btn.ghost { color: var(--accent); border: 1px solid var(--rule); } |
| 90 | .btn.ghost:hover, .btn.primary:hover { filter: brightness(1.1); } |
| 91 | .robot-note { |
| 92 | margin: 2.25rem auto 0; padding: 0.6rem 0.9rem; max-width: 36rem; |
| 93 | background: var(--accent-soft); border-radius: 0.5rem; |
| 94 | font-size: 0.9rem; color: var(--muted); |
| 95 | font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; |
| 96 | } |
| 97 | .robot-note a { color: var(--accent); } |
| 98 | |
| 99 | /* stat strip */ |
| 100 | .stats { |
| 101 | max-width: var(--wide); margin: 0 auto; |
| 102 | border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); |
| 103 | display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); |
| 104 | text-align: center; |
| 105 | } |
| 106 | .stats div { padding: 1.4rem 0.5rem; } |
| 107 | .stats .n { display: block; font-size: 2rem; font-weight: 700; color: var(--accent); line-height: 1.1; } |
| 108 | .stats .l { |
| 109 | font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; |
| 110 | font-size: 0.85rem; color: var(--muted); |
| 111 | } |
| 112 | |
| 113 | /* sections */ |
| 114 | main { max-width: var(--wide); margin: 0 auto; padding: 0 0 3rem; } |
| 115 | section { padding: 3rem 0; } |
| 116 | section + section { border-top: 1px solid var(--rule); } |
| 117 | section > * { max-width: var(--max); margin-left: auto; margin-right: auto; } |
| 118 | section > .cards { max-width: var(--wide); } |
| 119 | h2 { |
| 120 | font-size: 1.6rem; line-height: 1.25; font-weight: 700; |
| 121 | letter-spacing: -0.01em; margin-bottom: 0.4rem; text-align: center; |
| 122 | } |
| 123 | h2 a { color: inherit; text-decoration: none; } |
| 124 | h2 a:hover { color: var(--accent); } |
| 125 | .epigraph { |
| 126 | font-size: 1.15rem; font-style: italic; color: var(--muted); |
| 127 | margin-bottom: 1.4rem; text-align: center; |
| 128 | } |
| 129 | p { margin: 0 0 1.1rem; } |
| 130 | section > p:last-child { margin-bottom: 0; } |
| 131 | |
| 132 | /* cards */ |
| 133 | .cards { |
| 134 | display: grid; gap: 1rem; margin-top: 1.75rem; |
| 135 | grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); |
| 136 | } |
| 137 | .card { |
| 138 | border: 1px solid var(--rule); border-radius: 0.75rem; |
| 139 | padding: 1.25rem 1.35rem; |
| 140 | } |
| 141 | .card.feature { border-color: var(--accent); background: var(--accent-soft); } |
| 142 | .card h3 { font-size: 1.1rem; font-weight: 700; margin: 0 0 0.35rem; } |
| 143 | .card .kicker { |
| 144 | display: block; font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; |
| 145 | font-size: 0.78rem; font-weight: 600; letter-spacing: 0.06em; |
| 146 | text-transform: uppercase; color: var(--accent); margin-bottom: 0.3rem; |
| 147 | } |
| 148 | .card p { color: var(--muted); font-size: 0.97rem; margin: 0; } |
| 149 | |
| 150 | .keyline { |
| 151 | margin: 1.5rem auto; padding: 1rem 1.1rem; |
| 152 | border-left: 3px solid var(--accent); |
| 153 | background: var(--accent-soft); |
| 154 | border-radius: 0 0.4rem 0.4rem 0; |
| 155 | } |
| 156 | .keyline p:last-child { margin-bottom: 0; } |
| 157 | |
| 158 | ul.plain { list-style: none; margin: 1.1rem auto; } |
| 159 | ul.plain li { |
| 160 | padding: 0.45rem 0 0.45rem 1.25rem; position: relative; |
| 161 | } |
| 162 | ul.plain li::before { |
| 163 | content: "β"; position: absolute; left: 0; color: var(--accent); |
| 164 | } |
| 165 | |
| 166 | footer { |
| 167 | max-width: var(--wide); margin: 0 auto; |
| 168 | border-top: 1px solid var(--rule); |
| 169 | padding: 2rem 0 4rem; text-align: center; |
| 170 | color: var(--muted); font-size: 0.95rem; |
| 171 | } |
| 172 | footer p { margin-bottom: 0.6rem; max-width: var(--max); margin-left: auto; margin-right: auto; } |
| 173 | </style> |
| 174 | </head> |
| 175 | <body> |
| 176 | |
| 177 | <header> |
| 178 | <h1>The CARLOS Platform</h1> |
| 179 | <p class="tagline">Where CARLOS apps run.</p> |
| 180 | <p class="lede">A black-box deployment platform for <a href="https://carlosframework.com">CARLOS</a> apps: ship a release, and the platform places it on instances, routes to it, sleeps it when idle, and wakes it on demand. Heroku's ease, keeping CARLOS's promises β and the platform itself is one binary, a bucket, and DNS.</p> |
| 181 | <div class="cta"> |
| 182 | <a class="btn primary" href="#ship">What shipping looks like</a> |
| 183 | <a class="btn ghost" href="#status">How young is it?</a> |
| 184 | </div> |
| 185 | <p class="robot-note">π€ This page was written by an LLM, on the ideas, instruction, and editing of humans. AI-written text here is always marked, visibly, like this β that's <a href="https://11factor.org/#x">factor X</a>.</p> |
| 186 | </header> |
| 187 | |
| 188 | <div class="stats"> |
| 189 | <div><span class="n">1</span><span class="l">binary, playing every role</span></div> |
| 190 | <div><span class="n">0</span><span class="l">control-plane servers</span></div> |
| 191 | <div><span class="n">0</span><span class="l">rebuilds from canary to stable</span></div> |
| 192 | <div><span class="n">72h</span><span class="l">bake before stable adopts</span></div> |
| 193 | </div> |
| 194 | |
| 195 | <main> |
| 196 | |
| 197 | <section id="what"> |
| 198 | <p>The framework defines the shape of an app; the platform is the substrate it runs on β the way Heroku stood beside the twelve factors. An author ships <strong>releases</strong>: immutable, content-addressed artifacts. The platform places them on <strong>instances</strong>, routes to them through a mesh, hibernates them when idle, and wakes them on the request that needs them. Apps from many authors run side by side on the same fleet, sharing the same blind infrastructure.</p> |
| 199 | |
| 200 | <div class="keyline"> |
| 201 | <p>The whole self-hosting story is one sentence: <strong>a deployment is a bucket, N hosts running one binary, and DNS.</strong> Keeping it one sentence is a design constraint on everything else.</p> |
| 202 | </div> |
| 203 | |
| 204 | <p>There is no control-plane server to stand up, and none to be locked out of. All durable platform state β apps, releases, channels, instance records, leases β lives in an object-store bucket; hosts converge by watching it, and a burned-down box is replaced by pointing a fresh one at the bucket. The platform makes about itself the promise its instances already make: <strong>the box is disposable, the bucket is not.</strong> It is entirely open and trivially self-hostable, and two of its first three deployments <em>are</em> self-hosted β so anything the flagship needs that a self-hoster can't have is a design error, caught immediately.</p> |
| 205 | </section> |
| 206 | |
| 207 | <section id="ship"> |
| 208 | <h2><a href="#ship">Ship, canary, promote</a></h2> |
| 209 | <p class="epigraph">The dev pipeline is the prod pipeline, exercised on every push.</p> |
| 210 | |
| 211 | <p><code>carlos ship</code> uploads a release under its content hash. A <strong>channel</strong> is a per-app pointer to a release β <code>stable</code>, <code>beta</code>, and ephemeral <code>canary/*</code> channels β and advancing one is a single pointer write. Instances subscribe to a channel and roll onto new releases at their next natural boundary: wake from hibernation, or a drain-and-restart sweep. Rollback is the same operation backwards.</p> |
| 212 | |
| 213 | <div class="cards"> |
| 214 | <div class="card"> |
| 215 | <h3>Every branch gets a canary URL</h3> |
| 216 | <p><code>carlos canary</code> from a worktree builds, ships, creates a channel, spawns one instance bound to sandbox glue, and returns its URL β in seconds, not minutes. A dev environment is not a separate system; it is a channel with one subscriber.</p> |
| 217 | </div> |
| 218 | <div class="card"> |
| 219 | <h3>Promotion never rebuilds</h3> |
| 220 | <p><code>carlos promote</code> points a wider channel at the same hash: canary β beta β stable. Every environment running release <code>abc123</code> runs byte-identical code. What you tested is what ships, because it is the same bytes.</p> |
| 221 | </div> |
| 222 | <div class="card"> |
| 223 | <h3>Rollback is a pointer move</h3> |
| 224 | <p>A bad release hits gradually β instances adopt on wake β and pointing the channel back at the previous hash stops the bleed. Additive-only migrations mean old data stays readable under new code and new data survives the trip back.</p> |
| 225 | </div> |
| 226 | <div class="card"> |
| 227 | <h3>Static sites ride the same channels</h3> |
| 228 | <p>A static release is a content-addressed file tree the edge serves directly β no process, nothing to hibernate. Same canary URLs per branch, same promotion, same rollback. This page ships that way.</p> |
| 229 | </div> |
| 230 | </div> |
| 231 | </section> |
| 232 | |
| 233 | <section id="run"> |
| 234 | <h2><a href="#run">Sleeping is the default state</a></h2> |
| 235 | <p class="epigraph">Most software is used a few hours a week. The platform bills like it.</p> |
| 236 | |
| 237 | <p>An idle instance hibernates, and its database <strong>parks</strong> to object storage: a parked app is a few objects in a bucket β no process, no memory, no host doing anything on its behalf. The next request wakes it, on whichever eligible host answers first. Availability is continuous even when execution isn't.</p> |
| 238 | |
| 239 | <ul class="plain"> |
| 240 | <li><strong>Per-instance databases, continuously replicated.</strong> Every instance owns its own database, and it streams to the bucket from the first byte. Losing a host costs a restart, not a history: its leases expire and the instances wake elsewhere from their last parked state.</li> |
| 241 | <li><strong>Scheduling is deliberately dumb.</strong> No bin-packing optimizer β hibernation makes placement mistakes cheap. A badly-placed instance parks and wakes somewhere better next time.</li> |
| 242 | <li><strong>Scale is a declaration, not a rewrite.</strong> Parked β embedded SQLite β replicated β an attached server database (one per instance, inside the app's trust domain, never multi-tenant) β a dedicated pool. An app moves along that curve by changing its manifest, not its architecture.</li> |
| 243 | </ul> |
| 244 | </section> |
| 245 | |
| 246 | <section id="place"> |
| 247 | <h2><a href="#place">Placement you can point at</a></h2> |
| 248 | <p class="epigraph">Residency is enforced where the instance wakes, not promised in a policy PDF.</p> |
| 249 | |
| 250 | <div class="cards"> |
| 251 | <div class="card"> |
| 252 | <span class="kicker">Residency</span> |
| 253 | <h3>Choose where data lives</h3> |
| 254 | <p>An app β or a single instance, because one customer may demand EU while another doesn't care β declares <code>residency: eu</code>. A host outside the region will not claim the lease, so there is no code path where the instance runs elsewhere "just this once during a failover": no eligible host up means the wake fails loudly. Parked data, streams, and blobs rest in region-scoped buckets. Residency covers rest as well as execution.</p> |
| 255 | </div> |
| 256 | <div class="card"> |
| 257 | <span class="kicker">Dedicated pools</span> |
| 258 | <h3>Your own hardware, same platform</h3> |
| 259 | <p>Apps that hold real personal or payment data β not everything is end-to-end encrypted β can reserve hosts: their instances, parked data, and wake path only ever touch that pool, optionally with its own bucket, while still riding the shared edge, channels, CLI, and services.</p> |
| 260 | </div> |
| 261 | </div> |
| 262 | </section> |
| 263 | |
| 264 | <section id="services"> |
| 265 | <h2><a href="#services">Shared services: blind or seeing, never both</a></h2> |
| 266 | <p class="epigraph">Nothing that sees may store.</p> |
| 267 | |
| 268 | <p>Every shared service carries a declared class, visible in the manifest of every app that consumes it. <strong>Blind services</strong> may hold data, but only data they cannot read: the object store holds parked databases and blobs but no plaintext of E2EE apps; the vault holds sealed blobs whose keys live elsewhere; the push relay holds device tokens and timing but never notification content; the OAuth relay passes tokens through sealed to the requesting instance, never retaining them. What makes them blind is falsifiable the CARLOS way β publish their state and see what leaks.</p> |
| 269 | |
| 270 | <p><strong>Seeing services</strong> process plaintext β a URL unfurler, a PDF renderer, outbound mail β and are therefore forbidden to hold it. Not by promise: the platform grants a seeing service no database binding, no bucket write access, no persistent volume. Statelessness is imposed by construction. One honest caveat the mail service carries with it: the underlying email provider retains delivery metadata β bounces, complaints, suppressions β scoped per tenant.</p> |
| 271 | </section> |
| 272 | |
| 273 | <section id="supply"> |
| 274 | <h2><a href="#supply">Supply-chain honesty</a></h2> |
| 275 | <p class="epigraph">The scary step in any pipeline is the pointer that says "this is stable now."</p> |
| 276 | |
| 277 | <ul class="plain"> |
| 278 | <li><strong>Channel pointers are signed.</strong> A host agent rolls instances onto a release only when the pointer advancing the channel carries a valid signature β a bucket write alone is not a deployment.</li> |
| 279 | <li><strong>Releases bake before stable adopts them.</strong> A release must sit on its channel for 72 hours before stable instances roll onto it β time for a poisoned artifact to be noticed while its blast radius is still the people who opted in.</li> |
| 280 | <li><strong>Rollback never bakes.</strong> Pointing back at a hash that already served traffic takes effect immediately. The waiting period protects against new code, and going backwards is never new code.</li> |
| 281 | </ul> |
| 282 | |
| 283 | <p>Every release records its build attestation β commit and builder β under its hash, and every platform change ships through the platform itself: worktree, canary channel, a miniature canary deployment, a headless-browser gate that fails on any console error. The release machinery is tested by being the only way anything ships, including itself.</p> |
| 284 | </section> |
| 285 | |
| 286 | <section id="where"> |
| 287 | <h2><a href="#where">Three deployments, one platform</a></h2> |
| 288 | <p class="epigraph">The Open promise is proven from day one, because the first two production users are self-hosters.</p> |
| 289 | |
| 290 | <div class="cards"> |
| 291 | <div class="card"> |
| 292 | <h3>Tito</h3> |
| 293 | <p>Ticketing for conferences and events, running its own self-hosted deployment on its own account. Self-hosted use-case number one, and the platform's first proving ground.</p> |
| 294 | </div> |
| 295 | <div class="card"> |
| 296 | <h3>Eleven</h3> |
| 297 | <p>The encrypted messenger, on its own deployment β and the code quarry: the platform's edge, host agent, channels, and mergeable stores were extracted from Eleven's proven machinery.</p> |
| 298 | </div> |
| 299 | <div class="card feature"> |
| 300 | <h3>The flagship cloud</h3> |
| 301 | <p>The shared multi-tenant deployment, operated by the CARLOS authors, hosting Keymail, Woodstar, Slopbox, and Kass β the first four customers. Same open platform, differing from the self-hosted two in variables, never in shape.</p> |
| 302 | </div> |
| 303 | </div> |
| 304 | |
| 305 | <p style="margin-top:1.75rem">Apps live at <code><sqid>.oncarlos.com</code> β an address that never encodes region or host, so relocation and placement stay invisible to it. Custom domains ride on top as aliases.</p> |
| 306 | </section> |
| 307 | |
| 308 | <section id="status"> |
| 309 | <h2><a href="#status">Where it actually stands</a></h2> |
| 310 | <p class="epigraph">Fewer claims, all of them checkable β including this one.</p> |
| 311 | |
| 312 | <p>The platform is young. It was extracted from Eleven and titogo β systems that kept rebuilding the same deployment shape until it was obviously one thing β and its first production tenant, Kass, migrated onto it on 2026-08-01. Everything above describes the design being built and proven, not a mature product with years of scar tissue. If that honesty reads oddly on a homepage β good. It is the same trade the framework makes: the platform holds itself to the promises it hosts, and a promise you can't check yet is stated as exactly that.</p> |
| 313 | </section> |
| 314 | |
| 315 | </main> |
| 316 | |
| 317 | <footer> |
| 318 | <p>π€ Written by an LLM (Claude), on the ideas, instruction, and editing of humans. AI-written text here is always marked and always disclosed β see <a href="https://11factor.org/#x">factor X</a>.</p> |
| 319 | <p>The platform is the substrate for apps built on <a href="https://carlosframework.com">the CARLOS framework</a>, keeping the promises set out in <a href="https://11factor.org">The Eleven Factors</a> β the way Heroku stood beside the twelve.</p> |
| 320 | <p><a href="https://github.com/carlosframework/platform">Source for the platform β and this site β on GitHub</a>. Open, of course.</p> |
| 321 | </footer> |
| 322 | |
| 323 | </body> |
| 324 | </html> |
| 325 | |