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 CARLOS platform, member-side
2
3What `carlosframework/platform` does for an app, and the `carlos` CLI
4surface a member drives it with. Everything here works with **zero
5infrastructure access** β€” no SSH, no AWS console, no box commands. That is
6a design law, not a convenience: every command is built for a user who has
7nothing but the CLI and a browser. If a task seems to need a box, either
8you are self-hosting and operating the platform itself, or you have found
9a product gap to file β€” never a workaround to build.
10
11Snapshot date 2026-10-18, describing what Carloku serves: platform
12v0.28.0, released 2026-10-17. Verbs are stable, flag details evolve β€” trust
13`carlos <verb> -h` over this file, and remember a CLI can offer a flag the
14console you are talking to has switched off or does not have yet
15("Switched off, or not released" at the end). The CLI ships for macOS/Linux (brew,
16apt, static binaries) and Windows (client-only zip β€” no self-replace,
17`carlos update` defers to a fresh download).
18
19**Flags are written `--flag`.** Single-dash forms still parse and always
20will, but as of 2026-08-27 the documented face of every verb is the
21double dash, and the aliases are `-a` (`--app`), `-c` (`--console`), `-e`
22(`--environment`), `-v` (`--version`). Three flags turn up nearly
23everywhere and are not repeated below: `--app`, `--account` (or
24`--account-id` when you want a sqid matched as a sqid and nothing else),
25and `--console`, which you need only when this terminal is logged in to
26more than one deployment.
27
28## Start with `carlos auth whoami`
29
30Before concluding you cannot reach a CARLOS deployment, run it:
31
32```
33carlos auth whoami # the default console
34carlos auth whoami --console https://<console> # a specific one
35```
36
37It prints the identity, the terminal's label, the expiry, and every
38account with its sqid β€” which is also where you find the sqid your app's
39hostname needs.
40
41This is first because the obvious check is the misleading one. An agent's
42instinct is to `curl` the console and read the answer: the console will
43`302` you to a login page every time, because a browser session is not
44what the CLI holds. That redirect says nothing about whether you have
45access, and reading it as "no access" has already cost one session an
46afternoon and ended in a filed issue. **The CLI's credential is the only
47thing that answers the question.**
48
49Same for the deployment's identity: a self-hosted console is not
50`carloku.com`, and the console origin and the apps domain are different
51hostnames (Tito's are `carlos.tito.io` and `platform.tito.io`). If a
52project commits a `.carlos/config`, that names the console it belongs to;
53otherwise `--console` does.
54
55## What the platform owns (never hand-roll these)
56
57- **Routing and TLS** β€” the edge is the only process on :443; the route
58 table doubles as the ACME allowlist; per-host certs auto-obtained and
59 renewed, including customer domains and (via ACME delegation) wildcards.
60- **Replication** β€” Litestream on every instance database, run by the
61 platform's host agent; restore drills are the platform's job too. A
62 hibernating route's rung is tunable (`stream` β€” the default, about once
63 a second β€” / `batched`, five minutes / `daily`), a loss-window trade an
64 operator makes on a large, rarely-written database.
65- **Hibernation** β€” provisioned instances doze when idle and wake on
66 request; live and default-on, not a future feature. Cents-per-month
67 idle cost is the platform's economic story.
68- **Process supervision and restarts** β€” instances are converged from
69 bucket records; `carlos restart` cycles them in seconds, console-side.
70- **Config delivery** β€” `carlos env` / `carlos secrets` writes converge
71 onto boxes within seconds (a serial bump, 2s poll). No ssh-delivered
72 EnvironmentFiles.
73- **Deploy verification** β€” `carlos deploy` follows the rollout box by
74 box where boxes report it (they do on Carloku), and otherwise watches
75 the URL: the app's build probe if it declares one
76 (`carlos apps build-probe`), else the `X-Carlos-Version` header, which
77 the edge stamps from the route row the moment adoption lands, whether
78 or not the process has restarted yet.
79- **Outbound email** β€” the platform mints a sending identity on a domain,
80 publishes DKIM/SPF/DMARC where it controls the zone, and delivers working
81 SMTP credentials as config. Apps never hold an AWS key and nobody runs an
82 MTA. See "Sending email" below.
83- **Scheduled work** β€” the platform keeps the clock: a declared schedule
84 POSTs a tick to a path in your app at the time it is due, waking a
85 hibernating instance to run it. No in-app scheduler, no cron line on a
86 box. See "Scheduled work" below.
87- **Errors, availability and analytics** β€” the edge and the log stream
88 feed all three, with no SDK, no JavaScript, no cookie and no stored IP.
89 Errors and availability collect from the day an app ships; analytics is
90 opt-in. See "Watching an app" below.
91- **Password gate** β€” `carlos gate` puts one shared password in front of
92 an app's hostnames, enforced at the edge ahead of the cache and the
93 activator, so the app behind it never sees the cookie. A curtain over a
94 hostname, not accounts for the app's own users.
95- **DNS steering** β€” `carlos steering ... latency` opts a host into a
96 latency record per armed edge so the nearest one answers. A static site
97 on a platform hostname is steered from creation without asking; a
98 custom domain is not, because its DNS is yours.
99
100## Concepts
101
102- **Account** β€” the tenancy unit. Public short id (**sqid**, e.g. three
103 letters) appears in default hostnames; it is not a secret. Roles are
104 owner/member. `carlos auth whoami` shows yours.
105- **App** β€” named per account (not globally). Claimed with
106 `carlos apps create`; deleted apps sit in a 30-day trash.
107- **Release** β€” immutable, content-addressed, produced by the ship half
108 of `carlos deploy` (or by `carlos ship` on its own).
109 Kinds: `binary` (default) and `static`. Versions are free-form; git
110 short sha is the house convention.
111- **Channels and pipelines** (release pipelines v2, live 2026-08-22) β€”
112 **a new app is born with one channel, named `edge` by default**
113 (renameable at creation): that is its production, and `carlos deploy`
114 lands on it. Multi-channel ceremony is opt-in: a console-mediated
115 **pipeline** declares an ordered channel list (names are app-defined;
116 promotion onto channel N must come from Nβˆ’1; the first is the entry
117 channel) with per-channel, default-permissive policy β€” `bake`
118 duration, `passkey` step-up, `promote_approvals`, and
119 `change_approvals` (which also guards editing/removing the channel
120 and fast-tracking through its bake). `canary/<slug>` stays a reserved
121 platform namespace outside any pipeline: always allowed, zero bake,
122 per-session dead ends. Apps with no pipeline keep the legacy frozen
123 ladder (`edge β†’ beta β†’ stable`, holds 0/24h/72h, unconditional
124 passkey on stable β€” reaching stable cuts a semver tag). `--hotfix`, the
125 bake bypass, is an operator act on the direct-bucket path only: the
126 console refuses it by name, so a member never has it. Box-side, bake
127 changes **ratchet**: a shorter window is honoured only after the previously-known window has elapsed
128 once on the box's own clock, so a compromised console session cannot
129 collapse a hold and ship in the same hour.
130- **Production flag** β€” legacy: superseded by per-channel pipeline rules
131 for pipelined apps, still honoured by legacy boxes/apps. Its sharp
132 edge is recorded: a stable promote plus the console's default-checked
133 safety delay once left a hibernating app unwakeable for days β€”
134 ceremony belongs on channels you chose, not on defaults.
135- **Instance** β€” one account's running process for an app on a host.
136 Declared console-side (`carlos instances enable` once per app, then
137 `instances create --host …`); a box reconciler mints the actual route.
138 Exec-backed and hibernating by default. The process contract is
139 `<bin> --socket <path> --db <path>` on a unix socket β€” there is no
140 `$PORT` β€” and every instance serves `GET /healthz` and
141 `GET /api/version`. Every exec child is handed `CARLOS_HOST`, its
142 hostname exactly as `carlos instances list` shows it β€” **read it, never
143 the socket's file name**, which says nothing about the host on an
144 isolated box β€” and `CARLOS_REGION`, the placement code of the box it
145 runs on (`ie`, `au`, …), so an app that wants to know where it is can
146 read it rather than guess. Since v0.26.0 the platform also writes a
147 `CARLOS_INSTANCE_KEY` into the instance's own env before its first
148 start: a per-instance secret key for sealing the instance's own data.
149 It is not an API token and opens no platform door. On a box that has
150 isolated its instances (v0.25.0), each instance runs in a box of its
151 own: its own directory and namespaces, seeing only its own app's build,
152 under a memory ceiling set from the staged manifest plus CPU and IO
153 limits, and
154 with **no access to loopback or private addresses on the box** except
155 its DNS resolver β€” so an app cannot reach a sibling over `127.0.0.1`,
156 and one that grows past its tier is OOM-killed and restarted rather
157 than allowed to take the box's other tenants with it. On such a box the
158 working directory is the instance's data directory, the one its
159 database is in, so a relative path like `./data/blobs` lands beside
160 the database. **A static site has no
161 instance** β€” the edge serves it off the channel pointer, and giving one
162 an instance produces a route that can never wake ("Deploying a static
163 site" below).
164- **Config environments (bundles)** β€” `carlos env` / `carlos secrets`
165 write the app's **default** bundle; `--environment <name>` writes a
166 named one, layered on top of the default when config is materialized.
167 A route is *bound* to a bundle, and on a provisioned route that binding
168 lives in the instance record: `carlos instances create --environment`
169 at birth, `carlos instances set-environment` afterwards. (The box-local
170 `carlos route --environment` is reverted by the reconciler within
171 seconds and refuses such a row by name.) `carlos env environments`
172 lists an app's bundles, and `carlos restart --environment <name>`
173 cycles exactly the routes that read one.
174- **Canary** β€” `carlos canary` stands a second instance beside production
175 on a channel and hostname belonging to one branch: `canary/<slug>` and
176 `<slug>-<app>.<sqid>.<domain>` (one label since v0.24.0, so slug and
177 app together are at most 63 characters), the slug defaulting to the git
178 branch. Promotion into a canary is never refused and costs no rung β€” canary
179 channels sit outside every pipeline, so a build that has ridden one
180 still enters the pipeline fresh. Seven-day lease, refreshed on every
181 re-promote. It is a URL, not a traffic split.
182- **Service credential** β€” a bearer token a *server* holds rather than a
183 person: a CI job that ships, a signup flow that creates an instance per
184 customer. `carlos services create --role <r> --app <app> <name>` mints
185 one; reach is fixed at mint and bound to one account and (with `--app`)
186 one app. Roles, narrowest first: `deploy` (for CI: ships, and promotes
187 or rolls back only the rungs named at mint β€”
188 `services create --role deploy --app <app> --rungs canary,edge ci`,
189 never `stable`, and only `canary/<name>` channels on an app that
190 declares a pipeline; no hotfix, no app creation, so a new app's first deploy
191 still needs a person; CI sets `CARLOS_SERVICE_TOKEN` and
192 `CARLOS_CONSOLE` and runs `carlos deploy` as usual), `publish` (ship
193 releases), `instance` (create, delete, repoint and move the app's
194 instances, attach its domains, write its path routes, read its logs,
195 errors and analytics), `operate`, and `admin` (which includes minting
196 store IAM users β€” a real handover). No role can run
197 `carlos instances enable` or mint another credential. "Provisioning
198 instances from your own app" below.
199- **Path routes** β€” one hostname split by its first path segment:
200 `example.com/acme` served by one route, `example.com/globex` by another,
201 everything else by the hostname's own route. A prefix *points at* an
202 existing route and borrows its channel, config, hibernation and billing;
203 it defines nothing of its own. "One hostname, many instances" below.
204- **`.carlos/config`** β€” two layers, global `~/.carlos/config` and
205 per-project `./.carlos/config` (committed; nearest wins walking up).
206 Holds console, account, app, kind, artifact β€” the reason zero-argument
207 `carlos deploy` works β€” and optionally named `destinations`, one per
208 console or app a checkout deploys to, picked with `-d <name>` on any
209 verb that takes `--console` (`carlos config destinations` edits them). Project values are honored only when the file's
210 console matches the session's, so a committed config cannot silently
211 redirect someone else's credentials.
212- **Default addresses** β€” `<app>.<sqid>.<apps-domain>` (on the hosted
213 platform, `oncarlos.com`); canary form `<canary>-<app>.<sqid>.<domain>`.
214 Minted **alias** hosts intentionally carry no `X-Carlos-Version` β€”
215 verify on the canonical host.
216
217## Describe changes in Activity
218
219Agents pass `--message` explicitly for `ship`, `deploy`, `promote`,
220`rollback`, `restart`, `instances create`, `instances delete`, `features set`,
221`env set`, `env unset`, and `env exec-delivery`.
222Use a concise sentence describing the intended change or why it is needed:
223`carlos deploy --message "Fix invitation links after an email address changes"`.
224The console records the exact action, version, channel, or instance separately;
225repeating only a commit hash or "deploy app" adds no useful context. Do not
226invent an outcome you have not verified, or include secrets or config values.
227
228Messages are optional, one line, at most 240 Unicode characters. They are
229separate from release labels and release notes. Interactive CLI commands ask
230for a description by default; an empty answer skips it. Noninteractive commands
231do not prompt. `--no-prompt` suppresses the question for a command, and
232`"message_prompt": "off"` in `.carlos/config` disables prompts by default.
233Agents should still supply `--message` when
234prompts are disabled. Explicit `--message` still applies with `--no-prompt` or
235config suppression. `deploy` asks once and uses the same description for its
236ship and promote steps.
237
238## The member CLI
239
240| Verb | What it does |
241|---|---|
242| `carlos auth login\|whoami\|logout\|default` | Device-code login (approve in any signed-in browser); identity + memberships; per-project default console |
243| `carlos apps create\|list\|place\|set\|build-probe\|delete\|restore` | Claim an app (a name whose platform address is already held is refused, and `tmp-` names are reserved); `list` every app, the Trash included; place it on a customer fleet; `set --swap overlap\|hold` and `--swap-concurrency` choose how its instances take a new build ("Two builds at once"); `build-probe /api/version` makes `deploy` ask the process which build it runs rather than trust the header β€” worth setting on anything you deploy often; trash/restore |
244| `carlos deploy` | **Release new code β€” reach for this one.** ship + promote + watch until the new build serves; zero-arg with a saved project config, `-d <name>` for a named destination. Follows the rollout box by box and names a box where the build failed. `--channel` to land somewhere other than the entry channel (a canary); `--swap hold` for a release that cannot run beside its predecessor. Also the WHOLE story for a static site: `--kind static --host <h> <dir>` declares the route as well as shipping it ("Deploying a static site") |
245| `carlos ship` | The ship half alone: publish a release *without* releasing it (`--kind binary\|static`, `--version`, `--notes`); refuses a Go binary whose main package differs from the app's last release unless you pass `--program-change` (deploy has no such flag: ship, then promote); rate-limited per app (~2/minute β€” a 429 carries `Retry-After`) |
246| `carlos promote` | The promote half alone: move an **already-shipped** version onto a channel β€” a ladder step or a re-point, not how you release new code. The channel is positional, and optional when the app has only one. `--retry` re-tries a build that failed on some box, once you have fixed what it depended on. (`--hotfix` is refused by the console: an operator's direct-bucket act) |
247| `carlos canary` | Deploy onto a branch-only channel + hostname, waiting on the canary URL's own `X-Carlos-Version`; `ls` / `rm`. **Reach for `--environment`** if the app carries config β€” see "Standing a canary beside production" |
248| `carlos rollback` | Point a channel back at an earlier version |
249| `carlos pipeline` | Show or shape the app's release channels; `init --template edge-production\|full-ladder` replaces the single default channel with a starter pipeline |
250| `carlos channels` / `carlos releases` | What each channel serves / every shipped version; `releases retention` prunes old ones |
251| `carlos version target` | The semver family ships auto-increment under |
252| `carlos env` / `carlos secrets` | Plain vars / sealed secrets, layered per environment; `env sync` forces convergence; `secrets genkey` mints keypairs locally |
253| `carlos instances enable\|create\|list\|delete\|set-upstreams\|set-channel\|set-environment\|set\|update\|retry\|door-key` | Opt an app in (`enable --private-storage` gives every later instance its own environment and bucket); declare/inspect/remove instances; reorder or remove upstreams (**adding** an upstream address is operator-only since v0.25.0); move a route onto another channel or config bundle. `list` shows INSTALLED and SERVING builds and a REASON when they differ; `retry <host>` clears a failed build's hold on one instance; `set <host> --updates manual\|--pin <build>` and `update <host>` steer how one instance takes builds; `create --role replica --region <r>` makes a replica near buyers, holding a one-way copy of the live instance's data (the live instance names allowed regions with `--replicas` at its own create); `door-key` manages keys for the edge-local door, which a box operator must turn on. `--health running\|asleep\|not-responding` narrows a listing, which is usually what you want when something is wrong. For **binaries only** β€” a static site needs none of these, and `carlos deploy` declares what it does need |
254| `carlos restart` | Cycle an app's processes β€” no version or config change. `--host` narrows to one route, `--environment` to every route reading that bundle (the request that follows a `secrets set`), `--dry-run` prints the set. Sleeping instances are left asleep |
255| `carlos routes` | The app's routes through the console: upstream, channel, config environment, replication rung. It reads the console's own box, and says so on a multi-box fleet |
256| `carlos steering` | `latency` opts a host into a per-edge latency DNS record; `off` returns it to one static answer. Shared-pool instances only in v1 |
257| `carlos features set\|list` | The app's own feature flags β€” the platform stores and serves them, only the app interprets them. Lands on the instance's next `deployment`-block poll: no restart, no env edit, no box file. `key=` clears |
258| `carlos gate set\|list\|clear` | One shared password in front of the app's hostnames, custom domains included. `set` reads stdin when `--password` is absent; changing or clearing signs every visitor out |
259| `carlos errors` | One row per distinct error, newest first β€” `error`-tagged log lines plus the 500s the app itself answered, no wiring at all. `--fp <fingerprint>` expands a group; `--set-level warn` widens what counts |
260| `carlos availability` | Every 502, 503 and 504 the edge saw (the instance could not answer), grouped by cause, plus failed wakes and exits; one outage is one row. `--fp` expands a cause to each request. The Availability tab beside Errors |
261| `carlos analytics` | Page views, daily visitors, bytes, top pages/referers/events, counted at the edge. `--set-count edge` turns it on; `--range today\|7d\|30d` |
262| `carlos logs` | Merged app + platform + edge timeline (`-f` follows, `--grep`, `--since`) β€” no box access |
263| `carlos db pull\|query` | A read-only copy of an instance's database, restored from its replica on your machine; the live one is never touched. `--at <RFC 3339 time>` reads it as it was then (sleeping instances keep seven days of history). `--instance` when the app has several |
264| `carlos run --app <a> --instance <host> -- <command>` | Run one of your app's own commands as that instance, with its environment and database, output streamed as it arrives (v0.28.0). `--file` uploads files with the run (64 MB in total), `--timeout` (30 minutes by default, up to 6 hours), `--follow <id>` reattaches. A sleeping instance is woken; a live one keeps serving beside it; one run at a time per instance, refused while it moves or if its box is not yet on the nested layout. The way to run a one-off task against production without a box |
265| `carlos geo <address>` | What the `X-Carlos-Geo-*` headers would say for any address, with the credit the database requires |
266| `carlos sidecars enable\|create\|ls\|wake\|logs\|rm` | A process the platform runs for the app with no socket, hostname or DNS: a bot holding a connection out, a worker. Woken only by its schedule or `wake` |
267| `carlos config destinations` | Name the places one checkout deploys to in `.carlos/config`; `-d <name>` picks one |
268| `carlos domains attach\|detach\|verify\|list\|paths` | `attach --app <a> '*.<parent>'` (quoted) gives every instance a name `<label>.<parent>` with no per-instance claim; `<parent>` must be attached first and a `*` DNS record must point at the deployment. `verify` finds the TXT record a deployment that requires domain proof asks for. `paths <host>` prints the hostname's published path-route map (written through the API β€” "One hostname, many instances"). Claim customer hostnames (`--wildcard`, `--catchall`); prints the DNS records to create; certs follow automatically. Since 2026-08-27 the platform orders and renews those certs itself, and `list` joins the fleet's readings sweep, so DNS state, certificate expiry and delegation are readable from the terminal β€” a certificate the platform has given up on now says so instead of sitting pending forever |
269| `carlos store apply\|create\|status\|rotate\|scan` | Object storage. **Once per account, first:** an owner runs `store apply --account <a> --note "<why>"` and a deployment operator approves it β€” until then `create` has nothing to provision into, and `status` says the account is unapproved or pending. After approval `create` is the whole member workflow (unlimited stores, usage metered); `--environment <e>` gives one config bundle its own bucket and credential; credentials arrive as env; `rotate` is two-phase (`--finish` retires the old key). `scan` is opt-in virus scanning over the bucket's objects (`request`/`status` are yours, `grant`/`revoke`/`sweep`/`enforce` a deployment operator's) β€” and a scanner cannot read ciphertext, so a client-encrypted store is marked not applicable rather than given a coverage claim |
270| `carlos email enable\|status\|test\|domains\|events\|credentials\|rotate` | Declare sending; provision a verified domain (`domains rm` frees one); SMTP credentials arrive as env; `events` shows what became of each message for 30 days, `events push --path` posts them to the app, signed (`pause`/`resume` are a deployment operator's) |
271| `carlos relay enable\|status\|disable` | The platform's own STUN/TURN relay for an app's WebRTC calls. `enable` delivers `CARLOS_RELAY` (base64 JSON: per-region ICE URLs, a key, a username suffix) and the app mints a TURN credential per call from the key; `status` shows issue, pickup and per-region health. `enable` refuses, with a sentence, on a deployment that runs no relay. Never run coturn or buy a TURN service |
272| `carlos services create\|ls\|rotate\|revoke` | Server-held credentials (`--role deploy\|publish\|instance\|operate\|service\|admin`, `--app` to bind one app, `--rungs` for `deploy`); the secret prints once; `rotate` refuses the old secret on its next call, `revoke` stops the credential outright |
273| `carlos schedule ls\|set\|rm\|run` | Declare recurring work per app (`--every 6h` or `--cron "0 8 * * *"` β†’ a `POST` to a path your app serves); `ls` shows next/last per instance; `run` fires one now |
274| `carlos ledger append\|publish\|verify` | Open hash-chained per-app ledgers (the transparency machinery) |
275| `carlos accounts create\|list\|migrate` | Mint/list accounts; move an app between them. An account is *your* tenancy on the platform β€” never create one per customer of your app (customers are instances: "Provisioning instances from your own app") |
276| `carlos skills` | List the deployment's published agent skills (`/.well-known/agent-skills/index.json`) and fetch one, digest-verified, to stdout |
277| `carlos fleets create\|add-box\|rotate-token\|…` | Bring-your-own-boxes fleets that dial the console |
278| `carlos update` | Update the CLI binary itself (signature-verified; defers to brew/apt) |
279| `carlos vet` | Check a release against the platform contract |
280
281Box-side verbs exist (`edge`, `agent`, `adopt`, `route`, `add`, `ops`,
282`bootstrap`) but they are the *operator's* surface for running a platform
283deployment β€” a member never types them, and an agent reaching for them on
284a member task has taken a wrong turn.
285
286## Provisioning instances from your own app
287
288A product where every customer gets their own instance creates those
289instances itself, at signup, through the same console API the CLI uses.
290Nothing here needs a box, and nothing here is a workaround:
291
2921. **A person enables the app once** β€” `carlos instances enable --app
293 <tenant-app>`. This writes the manifest that bounds which domain, pool
294 and regions any instance may be created in, and no credential can do
295 it. Add `--private-storage` and every instance created afterwards
296 gets a config environment named after its host's first label with an
297 object store bound to it, provisioned by the ordinary create call (the
298 account needs an approved `store apply` first, and nothing is torn
299 down when an instance is deleted).
3002. **Mint the credential** β€” `carlos services create --role instance --app
301 <tenant-app> signup`. Store it with `carlos secrets set` on the app
302 that runs signup, **never on the tenant app itself**: every tenant
303 instance would then hold a token able to delete its siblings.
3043. **Create on signup** β€” `POST /api/cli/apps/<account>/<tenant-app>/instances`
305 with `Authorization: Bearer <credential>` and a JSON body; `host` is
306 required (one DNS label plus the manifest's domain), `region`,
307 `channel`, `environment` and `message` (the Activity description) are
308 optional. The console writes the
309 record and announces it, and a box mints the route within seconds.
3104. **Wait for the real thing** before sending the person there: poll
311 `https://<host>/api/version` for a 200 (a sub-500 answer is not
312 evidence), behind a zero-JS "getting your space ready" page that
313 refreshes itself.
314
315Give every label a random suffix (`<slug>-<6 random base32 chars>`, the
316label being the host's first DNS segment). That makes a collision with
317somebody else's instance a first-call event, so the one retry below can
318read its 409 without guessing.
319
320The answers to handle, from the apps that run this:
321
322| Status | Meaning | Do |
323|---|---|---|
324| `201` | Created | Wait for `/api/version`, then hand off |
325| `409` | That host already exists (or a sibling app owns the label) | Never treat it as yours: pick a new label. On a *retry of your own timed-out call*, a 409 means the first call worked |
326| `429` | The credential's own rate limit (it is separate from any person's, so a signup burst never starves your promotes) | Show a sentence and a retry; never loop |
327| `502` / timeout | The console could not complete the write | Retry once with the **same** host |
328| `400` | Bad body, or a host that is not one label plus the manifest's domain | A bug on your side; fail loudly |
329| `401` / `404` | Bad credential, or the wrong account or app (every miss is the same 404) | A configuration bug; fail loudly |
330
331Also on the same credential: `GET .../instances` (list), `GET`, `PATCH`
332(reorder or remove addrs, set the probe β€” adding an upstream address is
333operator-only since v0.25.0) and `DELETE .../instances/<host>`, and
334`POST/PUT .../domains` for customer hostnames. **Deleting an instance
335removes its record, never its data** β€” the parked database and blobs are
336left in place on purpose, and the call is idempotent.
337
338**The known gap:** the `instance` role is app-wide. One credential can
339create, delete or move *every* instance of the app, not only the ones it
340made. That is why it lives on the signup app alone, and why a signup app
341compromise must be named in your published trade-offs. A narrower,
342create-only credential is a filed platform request β€” do not build your own
343token broker around it.
344
345## One hostname, many instances
346
347Path routes let one hostname β€” your product's own domain β€” serve each
348customer from their own instance, so `example.com/acme` and
349`example.com/globex` are two processes and two databases behind one
350address. The hostname's own route keeps everything else (`/`, sign-in,
351marketing).
352
353- The hostname must be one the app holds: `carlos domains attach --app
354 <hub-app> example.com` first.
355- The map is written **whole** through the API, by a person or by the
356 hub app's `instance`/`publish` credential:
357 `PUT /api/cli/apps/<account>/<hub-app>/domains/example.com/paths` with
358 `{"paths": {"acme": "acme.<tenant-domain>", "globex": "globex.<tenant-domain>"}}`.
359 Keys are one bare path segment (no slash); values are hostnames of
360 existing routes. Publish the full map from your own records every time β€”
361 a lost delta desyncs silently, a whole republish repairs. `{"paths": {}}`
362 clears it; a body without the `paths` wrapper is refused, so a typo can
363 never wipe the map.
364- **Targets must belong to the same account** (they may be another app of
365 that account β€” a hub app pointing at tenant-app instances is the
366 intended shape).
367- **Targets must live on the same box as the hostname's own route.** A
368 target homed elsewhere is silently ignored and its prefix falls through
369 to the hostname's route. That caps a path-routed product at one box
370 today; cross-box path targets are a filed platform request.
371- A write goes live in about five seconds. `carlos domains paths --app
372 <hub-app> example.com` prints what was published; `carlos routes`
373 shows what the box actually applied.
374- The upstream receives the original path (`/acme/...`) unchanged, and the
375 target route is what is billed, versioned and woken.
376- **One hostname is one browser origin.** Cookies, storage and script
377 are shared across every prefix, so every tenant's pages can act on the
378 hub's session. That is fine while every tenant runs your app's code; if
379 tenants serve HTML their own customers wrote, give each tenant its own
380 hostname instead.
381
382## Deploying a static site
383
384A static site is one command, and the command is the same one binaries use:
385
386```
387carlos apps create --app mysite
388carlos deploy --app mysite --kind static --host mysite.<sqid>.oncarlos.com ./site
389```
390
391That ships the directory as an immutable release, promotes it to the app's
392entry channel, declares the route, and watches the URL until
393`X-Carlos-Version` reports the build. Nothing else is needed β€” not
394`carlos instances enable`, not `carlos instances create`, not a separate
395`carlos domains attach`.
396
397`--host` is required on the **first** static deploy and only that one. A
398static app has no instance record yet, so there is nothing to resolve the
399host from; that deploy writes the record, and afterwards plain `carlos
400deploy` resolves host and channel by itself. Give it a custom domain and
401the deploy attaches that too, printing the DNS records to publish.
402
403Expect the URL to take up to a minute after `promoted` prints. A static
404route trusts its resolved channel pointer for about 60 seconds, so the old
405page can outlive the promote by that long β€” `carlos deploy` already waits
406past one such window, and says so while it waits.
407
408### A static site has no instance, and must not be given one
409
410**This is the trap.** The verbs read like the binary recipe β€” enable an
411app for instances, create one, point it at a channel β€” and every step
412succeeds. The result cannot work.
413
414A static site is served by the edge straight off the channel pointer:
415there is no process, nothing hibernates, nothing wakes. An **instance** is
416a process the platform execs. A static release's manifest carries a site
417tarball and no build for any box's architecture, so an instance route on
418that channel finds nothing to run and fails every wake, permanently.
419
420Older platform builds reported that failure as *"nothing is promoted for
421this instance's channel"* β€” while `carlos channels` showed the promote
422sitting right there. Both statements were true and neither named the
423problem, which is the shape of the route, not the state of the channel.
424Current builds name it and refuse the create; if you meet the old message,
425this is what it means.
426
427The way out, on an app already in that state:
428
429```
430carlos instances delete --app mysite --host mysite.<sqid>.oncarlos.com
431carlos instances create --app mysite --host mysite.<sqid>.oncarlos.com --kind static --channel <ch>
432```
433
434The already-shipped release needs no re-ship β€” a static route reads the
435channel pointer, so the site serves within seconds of the record landing.
436
437### Channels belong to the app, not to the platform
438
439A new app is born with **one** channel, `edge` by default, and that one
440channel is its production. More are opt-in, declared by a pipeline, and
441named whatever the app calls them.
442
443So do not guess a channel name from the `canary β†’ edge β†’ beta β†’ stable`
444ladder in `--help`: that is the frozen legacy ladder an app inherits only
445when it declares no pipeline. `carlos channels` lists what this app
446actually has and `carlos pipeline` shows the order. `carlos promote
447<version>` with no channel resolves it when the app has one, and names the
448app's real channels when it has several.
449
450## Standing a canary beside production
451
452Review happens on a deployed canary, never on localhost, and one command
453does it:
454
455```
456carlos canary --app jam ./jam
457```
458
459The name defaults to the slugified git branch, so branch `new-queue`
460gives channel `canary/new-queue` and host
461`new-queue-jam.<sqid>.oncarlos.com`. An explicit `--name` is validated
462rather than quietly repaired, because the same string has to serve as
463both a channel segment and part of a hostname label: lowercase letters,
464digits, dashes, and at most 63 characters with the app name. `carlos canary ls` shows each canary's host, what it serves,
465which config bundle it is bound to and what is left of its seven-day
466lease; `rm` deletes the instance record and the host stops being served.
467The channel lapses on its own.
468
469**If the app carries config, pass `--environment`.** This is the trap,
470and it is worth stating in full because the failure looks like an app
471bug. Config in CARLOS is per-app, so a canary with no bundle of its own
472serves the app's *default* bundle β€” where every origin-shaped value names
473the app's **main** host. Anything keyed to that single scalar refuses
474requests on the canary hostname: the `Origin` header the app compares
475against, magic-link and callback URLs, a WebAuthn RP ID, the cookie
476`Secure` flag. The result is a canary that serves every GET perfectly and
477fails every form submission. Write the bundle first, then bind it:
478
479```
480carlos env --app jam --environment jam-canary JAM_ORIGIN=https://new-queue-jam.<sqid>.oncarlos.com
481carlos canary --app jam --environment jam-canary ./jam
482```
483
484The same applies to any second instance standing beside a live one, not
485just a canary β€” `carlos instances create --environment`, or
486`carlos instances set-environment` on a route that already exists,
487followed by a restart to pick the values up.
488
489## Watching an app: errors, availability and analytics
490
491All three read from the terminal or the app's dashboard tabs. **Errors
492and availability collect from the day the app ships** β€” nothing to turn
493on. **Analytics is off
494until you switch it on** (a deliberate ruling, 2026-09-07: counting
495visitors is the app's call, not the platform's).
496
497```
498carlos errors --app hello --since 24h
499carlos availability --app hello --since 24h
500carlos analytics --app hello --set-count edge
501carlos analytics --app hello --range 7d
502```
503
504`carlos errors` needs no wiring at all: it groups `error`-tagged log
505lines (or `warn` too, with `--set-level warn`) together with the 500s
506the app itself answered, one row per distinct error with a count and a
507location. `--fp <fingerprint>` expands one group to its occurrences.
508
509`carlos availability` (since v0.22.0) holds what used to sit in errors:
510every 502, 503 and 504 the edge saw, meaning the instance could not
511answer, grouped by cause (refused, closed mid-request, timed out, no box
512connected, or the app's own 503), with failed wakes and exits beside
513them. One outage is one row however many pages it hit; `--fp` expands a
514cause to each request and path. A visitor who gives up after five
515seconds or more without response headers is filed as an outage, so an
516endpoint that holds its headers by design, like a long poll, shows a
517standing row there, and that row is expected.
518
519`carlos analytics` counts at the edge as it serves the app: page views,
520daily visitors, bytes served, top pages, top referers, named events β€”
521**no JavaScript, no cookie, no stored IP**, which is why an app on CARLOS
522has no business reaching for a third-party analytics script. Visitors are
523a sketch, so a multi-day total is daily visitors summed rather than a
524deduplicated unique count. The switch is per-app or account-wide.
525Where the deployment publishes a GeoIP database, page views are counted
526by country too.
527
528Neither replaces `carlos logs`, still the merged app + platform + edge
529timeline (`-f`, `--grep`, `--since`, `--stream app`, `--host`) with no
530box access.
531
532## Sending email
533
534The platform issues SMTP credentials. An app declares that it sends, the
535console provisions an SES identity for a domain, publishes the DNS records
536where it controls the zone, and delivers working credentials as config. The
537app never holds a cloud key and nobody runs an MTA.
538
539One command does the whole walk β€” declare, provision the domain, wait for
540verification, deliver:
541
542```
543carlos email enable --app <app>
544```
545
546The identity defaults to the app's **own host**
547(`<app>.<sqid>.oncarlos.com`), so the signing domain matches the domain of
548the links inside the mail. Credentials arrive as env β€”
549`CARLOS_SMTP_HOST`, `_PORT`, `_USER`, `_PASS`, `_FROM`. Transport is
550submission on 587 with STARTTLS. `--env-prefix MAILER` renames the whole set
551to `MAILER_HOST`, `MAILER_PORT`, and so on, for an app that already reads
552its own names.
553
554For a customer's own domain:
555
556```
557carlos email enable --app <app> --domain mail.example.com
558```
559
560On the platform domain CARLOS publishes the records itself and verification
561is usually seconds. On a custom domain it prints the record set for the
562zone's owner to publish, then polls until SES confirms it (`--timeout`,
563default 10m; a gave-up wait exits non-zero and names what SES is still
564waiting for). `carlos email domains add` is the same provisioning step on
565its own, for adding a second sending domain to an app that already sends.
566
567The record set a customer has to publish is always these five, for
568`<domain>` and the region the identity was first verified in:
569
570| Name | Type | Value |
571|---|---|---|
572| `<token>._domainkey.<domain>` Γ— 3 | CNAME | `<token>.dkim.amazonses.com` |
573| `mail-<region>.<domain>` | MX | `10 feedback-smtp.<region>.amazonses.com` |
574| `mail-<region>.<domain>` | TXT | `v=spf1 include:amazonses.com ~all` |
575| `_dmarc.<domain>` | TXT | `v=DMARC1; p=none; adkim=s; aspf=s` (plus `rua=` if the deployment sets one) |
576
577The SPF record sits on the MAIL FROM subdomain rather than the identity
578domain because that is the domain SPF is checked against.
579
580**The published DMARC policy is `p=none`**, deliberately: a tenant whose
581very first send is rejected by its own DMARC record learns nothing useful
582from the failure. There is no `carlos email` verb that changes it. On a
583custom domain the zone is the customer's, so they can tighten it themselves
584whenever they choose β€” read the DKIM note below first.
585
586Then:
587
588| Verb | What it does |
589|---|---|
590| `carlos email status --app <app>` | What is declared, which domains verified, what was delivered |
591| `carlos email test --app <app> --to me@example.com` | Sends a REAL message through a throwaway credential, then revokes it |
592| `carlos email credentials create\|list\|revoke` | Standalone credentials for something not running on CARLOS; the password is shown **once** |
593| `carlos email rotate --app <app> [--finish]` | Two-phase key rotation β€” new key delivered, then the old one retired |
594
595### Three things that will bite
596
597- **A freshly minted credential does not work for the first minute or
598 two.** SES's SMTP endpoint does not see a new IAM access key
599 immediately, and the symptom is `535 Authentication Credentials Invalid`
600 β€” which reads exactly like a wrong password and is not one. Wait and
601 retry. An app's first sends straight after `enable` hit this too, so do
602 not diagnose a broken credential from the first minute of logs.
603 **A 535 that outlasts a few minutes is a different problem** and is worth
604 treating as real: check the key is still `Active` and not paused
605 (`carlos email status`), and that the password was derived for the same
606 region as the SMTP host β€” the derivation is region-scoped, so a
607 credential minted for one region never authenticates against another's
608 endpoint.
609- **DMARC alignment rests on DKIM alone.** Records are published with
610 `adkim=s; aspf=s`, and that strictness is what stops one tenant's
611 signature authenticating another tenant's From address on a shared
612 domain. SPF authenticates but does **not** align: the envelope sender is
613 a region-qualified subdomain (`mail-<region>.<domain>`), which strict
614 alignment treats as a different domain. DMARC therefore passes on the
615 DKIM rail only. That is normal for SES β€” but it means DKIM is the single
616 rail, so never turn off DKIM signing on a sending identity. If a customer
617 tightens their own zone to `p=reject`, that is safe but unforgiving: a
618 DKIM CNAME later dropped from their zone stops being a downgrade and
619 becomes total delivery failure.
620- **The MAIL FROM subdomain carries its region forever.** It is fixed at
621 the first verification (`mail-eu-west-1.<domain>`). On a custom domain
622 that record lives in the customer's zone β€” the one record CARLOS cannot
623 republish for them β€” so moving regions later means going back to the
624 customer for a DNS change.
625
626## Scheduled work
627
628The platform keeps the clock. A hibernating instance cannot run an
629in-process timer, so recurring work is declared from outside the app and
630delivered as a **tick**: a `POST` to a path you declared, at the time it
631is due, waking the instance first and holding it awake while your handler
632runs. Nothing else crosses the boundary β€” no payload, no job state, no
633queue. Live 2026-08-24.
634
635Declare it with the CLI, not in code:
636
637```
638carlos schedule set --app <app> --name sync --every 6h --path /jobs/sync
639carlos schedule set --app <app> --name reminders --cron "0 8 * * *" --path /jobs/reminders
640```
641
642`--every` is a Go duration, whole minutes, 1m to 30d. `--cron` is five UTC
643fields (`min hour dom month dow`, with `*`, `n`, `a-b`, `*/n`, `a,b`) β€”
644no names, no `@daily`, no seconds. Names are
645`^[a-z0-9][a-z0-9-]{0,31}$`, at most 20 per app. `carlos schedule ls`
646prints the declaration and then what each instance reports (`next`,
647`last`, `last_status`); `run` requests an out-of-band fire on top of the
648normal rhythm; `rm` removes the declaration. The verbs say **recorded**
649and **requested**, never "fired": they write one console object, and each
650box acts on it within seconds.
651
652The app's whole part is a handler on that path:
653
654```go
655// POST /jobs/sync
656func handleSync(w http.ResponseWriter, r *http.Request) {
657 if !carlos.Tick(r) { // amadan.net/rastrillo/rastrillo/carlos
658 http.Error(w, "forbidden", http.StatusForbidden)
659 return
660 }
661 if occ, ok := carlos.TickOccurrence(r); ok && alreadyDone(occ) {
662 w.WriteHeader(http.StatusNoContent)
663 return
664 }
665 if err := syncer.RunOnce(r.Context()); err != nil {
666 http.Error(w, err.Error(), http.StatusInternalServerError) // 5xx = retry
667 return
668 }
669 w.WriteHeader(http.StatusNoContent)
670}
671```
672
673`carlos.Tick` is the constant-time compare of the bearer against
674`$CARLOS_ADMIN_TOKEN`, which the platform mints into the instance's
675environment; use it rather than hand-rolling the check. A tick arrives on
676an ordinary public route, so a tick nobody authenticated is an internet
677request to that path β€” and the `X-Carlos-*` headers are not evidence,
678anyone can set those. The status is your whole reply: **2xx** done,
679**5xx** retry with backoff (five attempts, 1m doubling to 15m, then the
680schedule advances), **4xx** don't bother.
681
682One-shots are the app's own call, over the control socket the agent binds
683before your process starts:
684
685```go
686err := carlos.ScheduleAt(ctx, "remind-"+id, when, "/jobs/remind") // carlos.ScheduleCancel drops it
687```
688
689Same tick, same handler, same name namespace as declared schedules (a
690collision returns `ErrDeclaredSchedule`). Up to 400 days ahead, 1000
691pending per host. Reach for one only when the work genuinely has a
692specific time; a daily schedule whose handler asks its own database
693"what falls today?" beats one timer per row.
694
695### Things that bite
696
697- **Do the work inside the request.** The instance is held awake for
698 exactly as long as the request is open (30-minute ceiling) and the idle
699 clock starts the moment you return. Reply 202 and finish in a goroutine
700 and that goroutine gets hibernated mid-job.
701- **Dedupe on `X-Carlos-Schedule-At`** β€” unix seconds, read it with
702 `carlos.TickOccurrence`. It is the instant the delivery is *for*, so
703 every retry of one failed occurrence carries the same value, as does a
704 redelivery after a box crash or a deploy that cut a long job. Never key
705 on the wall clock: a retry twenty minutes later is the same occurrence.
706 Keep `Tick` as the guard β€” a request with no occurrence header means
707 "no dedupe key, run it", which is what your own "Sync now" button looks
708 like.
709- **A 4xx is never retried.** A path with no handler, or a handler that
710 refuses a token it cannot see, records `app refused` and the schedule
711 advances to its next occurrence. That failure is quiet β€” `next` still
712 moves, so `ls` reads like a success. Check `last_status`.
713- **An instance running since before the platform roll refuses its own
714 ticks.** The token is minted at spawn, so a process older than the roll
715 has no `$CARLOS_ADMIN_TOKEN`, `Tick` is always false, and
716 `carlos.ScheduleAt` returns `ErrUnauthorized`. `carlos restart` clears
717 it; nothing else does.
718- **A schedule tighter than the idle window keeps the instance resident,
719 and residency is billed.** Since 2026-08-31 a tick no longer stamps
720 activity, but the sweeper deliberately will not put a host to sleep
721 when its next tick is due sooner than the idle window (15 minutes by
722 default) β€” staying awake is genuinely cheaper than a wake/sleep cycle
723 per tick, each of which is a full SQLite checkpoint-and-restore. So a
724 5-minute schedule against a 15-minute window never sleeps and accrues
725 instance-hours around the clock; an hourly one sleeps about three
726 quarters of each hour. `carlos schedule ls` and `set` now say so in one
727 line rather than leaving it to be found on a bill. The platform warns
728 and never refuses: a tight schedule is a legitimate thing to want.
729- **Unit-backed instances are not delivered to** in this release β€” `ls`
730 says `unsupported` against them. Exec-backed instances (the default)
731 and sidecars work.
732- **Once-timers outlive the process.** They live in the box registry, so
733 a restart loses none, and re-asserting them at boot is safe because a
734 repeated name replaces rather than adds. Off-platform there is no
735 control socket at all: `ScheduleAt` returns `ErrNotOnCarlos`, which
736 boot code should treat as "skip", not as a failure.
737
738## Two builds at once
739
740Since v0.26.0, where the deployment has switched it on (Carloku did on
7412026-10-14; the release default is off, and then a build is taken by
742restart and `deploy` watches the URL instead of each box's rollout), a
743new build reaching an awake instance starts beside the old one and takes over once it passes
744its health check, so no request waits (`--swap overlap`, the default).
745For a few seconds both builds run against the same database and the same
746scheduled work, and the new build's migrations run while the old one
747still serves. That makes two rules part of every release:
748
749- **Migrations stay additive across two consecutive releases.** Add a
750 column, table or index in one release; drop the old one in a later
751 release, once no running build reads it. A rename is an add now and a
752 drop later.
753- **Schedule names stay the same across the two builds**
754 (`carlos schedule`).
755
756A release that cannot follow them ships with `--swap hold` on
757`carlos deploy` or `carlos ship`: each instance stops before its new
758build starts, and requests wait a few seconds for it.
759`carlos apps set <app> --swap hold` makes that the app's rule, and
760`--swap-concurrency` is how many instances on one box swap at once,
761after the first has stayed up for ten seconds. If a build fails on a box,
762that box stops rolling it out, its other instances stay on the build they
763were serving, and `carlos deploy` names the failure and exits non-zero;
764`carlos instances list` shows it under REASON. Fix the cause, then
765`carlos promote --retry <version> <channel>` (every box) or
766`carlos instances retry <host>` (one instance).
767
768## Visitor location headers
769
770On a deployment that publishes a GeoIP database (since v0.25.1),
771every request the edge passes to an app carries the visitor's rough
772location: `X-Carlos-Geo-Country` (ISO 3166-1, `IE`),
773`X-Carlos-Geo-Subdivision` (ISO 3166-2, only some databases),
774`X-Carlos-Geo-Lat` / `-Lon` (two decimals: an area, not a position) and
775`X-Carlos-Geo-Source` (which database answered). Any of them can be
776missing: treat that as unknown, never as an error. The edge strips any a
777visitor sends, so the values are always the platform's. A cached page
778that differs by country sends `Vary: X-Carlos-Geo-Country`; never vary on
779the coordinates.
780
781**An app that shows a visitor anything worked out from these headers
782must credit the database `X-Carlos-Geo-Source` names** β€” `DBIP-…`:
783"IP Geolocation by DB-IP", linked to https://db-ip.com; `GeoLite2-…`:
784"This product includes GeoLite2 data created by MaxMind, available from
785https://www.maxmind.com". For an address that is not the current
786request (a log line, a webhook), `carlos geo <address> --app <a>`, or
787from an app `GET /api/cli/apps/<account>/<app>/geo?ip=<address>`, at
788most 60 a minute. That call needs an `instance`-role token, which is
789app-wide (it can also create, repoint and delete every sibling
790instance), so make it only from a central app that already holds one;
791never deliver that token to a tenant app's instances just to look up an
792address. Tenant instances use the request headers.
793
794## Deploy truths (each paid for at least once)
795
796- **`carlos deploy` is the release motion**: ship, promote to the entry
797 channel (or the channel the app's instances follow), then follow each
798 box's rollout (or, where a box does not report one, watch the URL)
799 until the shipped build is serving. "Held for bake" on a
800 channel that declares one is the system working, not failing.
801- **A promote is not a deploy** until the process cycles. An awake
802 instance starts the new build within seconds of its box adopting it
803 (beside the old one where two builds at once is on), an asleep one
804 takes it on its next request, and unit-stamped routes are restarted.
805 A bespoke long-running unit
806 outside the platform's knowledge stays old until *something* restarts
807 it β€” know which of the three your app is.
808- **Verify against the thing you changed, with the binary you built.**
809 A 200 is not proof: an app-shell route returns 200 HTML for
810 `/api/version` and will happily "verify" any build ever shipped. The
811 proof is the build the process itself reports: set
812 `carlos apps build-probe`, or read `X-Carlos-Version` on the canonical
813 host knowing the edge stamps it before the process has cycled; stale
814 DNS and stale local binaries have both produced false "verified"
815 reports.
816- **Review on a canary, not on localhost.** `carlos canary` cannot
817 disturb production: its channel sits outside every pipeline and its
818 hostname is the branch's own. Bring a config bundle with it if the app
819 has one.
820- **When CI ships for you, verify by ancestry, not equality** β€” a PR
821 merging behind yours cancels your run and ships a commit *containing*
822 yours.
823- **Stamp your build version.** For rastrillo apps,
824 `-ldflags "-X amadan.net/rastrillo/rastrillo.BuildVersion=<sha>"`
825 β€” or every release's `/api/version` reports `dev`.
826
827## Switched off, or not released
828
829Released, switched off on Carloku β€” the CLI has the flag, the console
830declines or ignores it, and nothing should be built to stand in for
831them: app residency (`--region` on `apps create`, `deploy` and `canary`
832since v0.23.0; the console starts the app in its home region and says
833so), instance moves (`carlos move`, v0.26.0; it sends nothing), and
834temporary static sites (`deploy --temporary` / `--ttl`, `apps ttl|keep`,
835v0.25.1). Names starting `tmp-` are reserved whether or not temporary
836sites are on.
837
838Released in v0.28.0 but refused until an operator turns them on:
839seeded instances (`instances create --from <host>` starts a new
840instance from a copy of another's database; `instances import` brings
841in one the platform never ran). Not built: an
842instance-bound token in place of the app-wide `instance` role, a
843create-only provisioning role, and cross-box path targets. Check
844`carlos <verb> -h` against the console and wait.
845
846## Self-hosting the platform
847
848The platform is the same software members deploy onto: a self-hosted
849deployment (Tito's path) runs its own console, boxes, and bucket in its
850own cloud account, and its members use the identical `carlos` CLI pointed
851at that console. Operating it is real work β€” box provisioning, the
852adopt/restart cadence, platform rolls β€” and it is the one context where
853box verbs and cloud access are legitimate. Commit a `.carlos/config`
854naming the deployment's console so sessions cannot fall back to the wrong
855one; the CLI's account default is not your account (`ops` exists on every
856deployment and is empty β€” a `not found` against it once cost an hour of
857confident wrong diagnosis).
858
859The deployment's own binary is a channel like any app's:
860`carlos system update --channel canary|stable` promotes a release, and
861every box on that channel fetches it, verifies it against the signed
862pointer, installs it, restarts its edge, and rolls itself back if the new
863one does not come up and stay up. `carlos system status` shows each box's
864running build and last apply outcome; `carlos system rollback` undoes it;
865`--spread 10m` staggers a stable roll. The console is an app β€” update it
866with `carlos deploy --app console`. Owner of the ops account only.
867
868For the underlying machinery β€” registry, router, replication, hibernation
869internals β€” see blueprint.md, which is the reference for what the
870platform does on your behalf and for hand-rolling outside it.
871