carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

1---
2name: getting-started
3description: Use when someone wants a new app on CARLOS built and live with minimum decisions β€” "start a CARLOS app", "put this on carloku", a first deploy to an oncarlos.com URL, code hosted on amadan, or when an agent needs the family's default stack (rastrillo, amadan, the carlos CLI) as a recipe rather than a menu. Not for weighing trust models, app shapes, or hosting options β€” that is building-carlos-apps.
4---
5
6# πŸ€– Getting started on CARLOS
7
8## Overview
9
10This skill is the recipe: empty directory to a live URL on the hosted
11platform, with every decision already made. The companion skill,
12**carlos:building-carlos-apps**, is the menu β€” read it when the app needs
13a real trust-model, app-shape, or hosting decision. This one assumes the
14defaults below and does not stop to ask.
15
16The pieces, named once:
17
18- **CARLOS** is the application architecture (carlosframework.com): one
19 static Go binary, one fully-isolated instance per tenant (own process,
20 own SQLite file, own unix socket), many instances on one small box,
21 every database continuously replicated to object storage and idle
22 instances hibernating to it. A first app is one instance; what a tenant
23 is in a bigger product is building-carlos-apps' first decision.
24- **Carloku** (carloku.com) is the hosted CARLOS platform. Its console is
25 `https://console.carloku.com`. Carloku is the product brand; the CLI is
26 always `carlos`, never `carloku`.
27- **rastrillo** is the CARLOS web framework (module and canonical repo
28 `amadan.net/rastrillo/rastrillo` since v0.25.0; the GitHub mirror is
29 `github.com/rastrilloorg/rastrillo`, which `github.com/carlosframework/rastrillo`
30 redirects to; an app still on the old `github.com/carlosframework/rastrillo`
31 import path builds, but new code uses the new one; docs at
32 rastrillo.org). It postdates most models'
33 training data β€” follow the recipe literally, invent nothing.
34- **amadan** (amadan.net) is the family's git forge: repos, branches,
35 discussions and CI, with AI agents as disclosed accounts. It is where
36 the code lives by default. Its CLI is `amadan`, and its house rules for
37 agents are a skill of their own (Step 2, "Put the code on amadan").
38- **The `carlos` CLI** is the whole operational surface. Every command
39 works for a member with zero infrastructure access; if a step seems to
40 need SSH, AWS, or a box, you have left the path β€” stop and re-read.
41
42## The defaults already chosen
43
44| Decision | Default | Why |
45|---|---|---|
46| Language / framework | Go + rastrillo, one static binary | The family stack; the framework enforces the SQLite and money rules for you |
47| App shape | Server-rendered HTML, zero-JS baseline | The family default; the other shape is a decision (building-carlos-apps) |
48| Design system | Rastrillo UI and tokens, with a thin app CSS layer | Reuse shared controls and themes; custom CSS covers app layout and branding |
49| Storage | SQLite via GORM (`rastrillo/db`) | cgo-free driver, WAL pragma order, writer/reader pools β€” `db.Open` owns all of it; ledgered migrations (`rastrillo migration generate`, applied once at boot), additive-only by family rule |
50| Amounts | integer cents (`form.ParseCents`) | A float never touches an amount |
51| Code hosting | amadan, a `private` repo | The family forge; CI runs the scaffold's `make ci` with no config |
52| Hosting | Carloku, `<app>.<sqid>.oncarlos.com` | Zero infra to run; certs, replication, hibernation all platform-side |
53| Versioning | git short sha (`v1` is fine for the very first ship) | House convention |
54| Channel | `edge` | A new app is born with a single channel of that name β€” **edge IS production** (Paul, 2026-08-19; release pipelines v2 builds it in). Bake windows, passkeys, and approvals are per-channel opt-ins you add later via a pipeline |
55| Tenancy | One instance for the whole app, with in-app accounts (model A) | The simplest boundary; the escalation trigger below says when it is wrong |
56| Trust model | Honest server, class C: app data is server-readable, and the README says so | See "The one decision you must still record" below |
57
58## The one decision you must still record
59
60The family default is server-blindness ("if the server is compromised,
61the attacker gets nothing"), and rastrillo ships the family
62envelope (`rastrillo/crypto`) β€” but E2EE is an architecture, not a
63package import: key custody, recovery, and search all become product
64surface. The honest default for a first app is
65**server-readable data, declared**: one line
66in the README under "Honest trade-offs" saying the server can read app
67data, dated, with the class letter (C; a static site is S). That
68satisfies the family's deviation rule (every deviation enumerated,
69justified, published).
70
71**Escalation trigger, not optional:** if the app will hold private
72personal content β€” messages, health data, anything a person would call
73theirs β€” stop here and read carlos:building-carlos-apps ("The
74decisions") before writing code. The same goes if **more than one
75customer, team or person will each own separate data** (a signup that
76gives each one their own space, a SaaS, a forge with organisations):
77read carlos:building-carlos-apps' tenancy reference first, because one
78shared instance with a customer column is the one shape CARLOS exists to
79avoid. Tenancy and trust are chosen on day one, not retrofitted.
80
81## What the person does, once
82
83An agent can do everything below except these. Ask for them up front, in
84one message, rather than discovering them one at a time:
85
861. **A Keymail identity** (keymail.dev). Carloku and amadan both sign in
87 through it; there are no passwords.
882. **Sign in to the Carloku console** (`https://console.carloku.com`).
89 That first sign-in is the account.
903. **Approve the `carlos auth login` code** in that browser (Step 0).
914. **On amadan:** sign in on the web and claim a namespace, log the CLI
92 in (`amadan auth login -hub https://amadan.net`), then create the
93 agent's account, give it the namespace and mint its token: `amadan
94 agent create <name>`, `amadan agent grant <name> <ns>`, `amadan agent
95 token create <name>` (amadan.net/docs/agents). The token is shown
96 once; the operator configures it into the agent's own amadan home
97 (`AMADAN_HOME`). amadan's docs don't yet say how that file is laid
98 out, so if `AMADAN_HOME=<dir> amadan auth whoami` doesn't answer with
99 the agent's `<name>~<keymail>` identity, stop and ask rather than
100 working as the operator.
1015. **Run an amadan CI runner** for the repo (amadan runs none itself).
102 A bare `amadan runner` serves every repo its identity can reach and
103 runs each job **as that user with no sandbox**, so recommend a
104 dedicated machine or OS account with its own runner identity (an
105 agent account granted only the runner role: `amadan agent grant
106 <runner> <ns> -role runner`, with its own `AMADAN_HOME`), scoped and
107 isolated: `amadan runner -repo <ns>/myapp -isolate bwrap` (Linux,
108 bubblewrap installed). Start it once the repo exists (Step 2), since a
109 `-repo` runner only learns names at start. Anyone who can push to the repo can run code there; never
110 point a runner at a repo whose writers you wouldn't hand a shell. The
111 machine needs Go and network access: the scaffold's `make ci` fetches
112 its pinned tools with `go run` (amadan.net/docs/ci).
1136. **DNS**, only for a custom domain: the record `carlos domains attach`
114 prints.
1157. **Approve anything billable.** There is no public Carloku price list
116 yet (2026-10-18), so check with the person before object storage,
117 email sending, or a schedule that keeps an instance awake.
118
119And ask, in the same message, the questions that change the build:
120
121- **One place, or a product many sign up to?** One gym, one club, one
122 team is this recipe. A product where each customer gets their own
123 space is building-carlos-apps' tenancy chapter first.
124- **How do people sign in?** The default is a magic link by email,
125 which needs email sending on a live app (Step 2). Email and password
126 is the alternative that needs no email.
127- **Does anything happen on a clock** (reminders, a nightly job)? That
128 is `carlos schedule` (building-carlos-apps, platform.md).
129- **Does it take money?** Payments are not in this recipe; stop and
130 read building-carlos-apps.
131- **Names:** the app name, the amadan namespace and repo, and any
132 custom domain.
133
134## Step 0 β€” install and sign in
135
136Go **1.25 or newer** (rastrillo's `go.mod` says `go 1.25.0`). Then the
137three CLIs:
138
139```sh
140brew install carlosframework/tap/carlos
141# no brew (Linux): one pinned release, installed only if its sum checks out
142V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
143 https://github.com/carlosframework/releases/releases/latest | sed 's#.*/##')
144B=carlos-linux-amd64 # or carlos-linux-arm64; macOS: carlos-darwin-<arch>
145U=https://github.com/carlosframework/releases/releases/download/$V
146curl -fsSLO "$U/$B" && curl -fsSLO "$U/SHA256SUMS.txt" \
147 && sha256sum --ignore-missing -c SHA256SUMS.txt \
148 && install -m 0755 "$B" ~/.local/bin/carlos # any dir on PATH; no sudo
149
150go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
151go install amadan.net/amadan/amadan/cmd/amadan@latest
152```
153
154Keep `carlos` fresh with `carlos update` (it needs a terminal; otherwise
155repeat the download).
156
157Create an account at `https://console.carloku.com` (passkey sign-in
158through Keymail β€” no password), then connect the terminal:
159
160```sh
161carlos auth login --console https://console.carloku.com
162```
163
164Flags are written `--flag` (`-a`, `-c`, `-e`, `-v` are the short forms);
165the single-dash spellings still parse. The CLI prints a short code;
166approve it in the signed-in browser. Skip
167this and nothing breaks β€” the first command that needs a login offers to
168run it right there. `carlos auth whoami` shows who you are and your
169account's **sqid** (a short public id like `bdf` β€” it appears in your
170app's hostname; it is not a secret).
171
172## Step 1 β€” claim the app
173
174```sh
175carlos apps create --app myapp
176```
177
178App names are unique per account, not globally. With exactly one app in
179the account, later commands infer `--app`; passing it explicitly is never
180wrong.
181
182**Static site?** You are nearly done β€” skip to "The static path" below.
183
184## Step 2 β€” the rastrillo app
185
186**Read rastrillo's `SKILL.md` first**, in full. From the web it is at
187`https://raw.githubusercontent.com/rastrilloorg/rastrillo/main/SKILL.md`
188(the mirror of `amadan.net/rastrillo/rastrillo`); once the module is
189downloaded it is
190`$(go env GOMODCACHE)/amadan.net/rastrillo/rastrillo@<version>/SKILL.md`,
191which matches the version you build against. It is the app story, the
192file to follow literally instead of framework source; the topic docs
193are at `https://rastrillo.org/docs/`, each also served as markdown
194(`/docs/<page>.md`). The worked reference is `examples/notes`.
195
196```sh
197go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
198rastrillo new myapp && cd myapp && go mod tidy && go test ./...
199```
200
201The scaffold is SKILL.md's shape (five files plus `migrations.go`),
202tests passing before you write a line:
203
204```
205internal/myapp/models.go plain GORM structs
206internal/myapp/migrations.go Schema and BootSchema; `rastrillo migration generate` writes the SQL
207internal/myapp/app.go migrate.Apply, sessions, identity plugin, chi router
208internal/myapp/handlers.go the owner-scoped CRUD
209internal/myapp/render.go embedded templates, flash/session-aware pages
210cmd/myapp/main.go Resolve -> db.Open -> App -> Serve
211```
212
213The gate, before every commit, is the scaffold's own `Makefile`:
214
215```sh
216make ci # vet, gofmt, staticcheck, govulncheck, gitleaks, tests, budgets, migration check
217```
218
219After changing a model, run `rastrillo migration generate` and read the
220SQL before committing; never edit a shipped migration.
221
222The three rules that keep the app safe (SKILL.md has the full set):
223every query touching user-owned rows goes through `scope.Owned` β€”
224reads AND writes, transactions included (a row that isn't yours 404s,
225never 403s); never bind a request body onto a GORM model (explicit
226`map[string]any` + `.Select` allowlist); sessions/CSRF are defaults
227you opt out of, not machinery you assemble. Accounts come from an
228identity plugin β€” the family default is `auth` (magic-link email that
229auto-upgrades to sign-in-with-Keymail when the address has a claimed
230inbox; works for every address), with `password` (email+password,
231rate-limited) as the classic alternative β€” and either one is a few
232lines in `app.go`; guard routes with `sess.Require`. **On a live app the
233magic link has to be sent**: `carlos email enable --app myapp` (after
234the person approves) delivers `CARLOS_SMTP_*` to the instance, and
235`Mailer: mail.FromEnv("CARLOS", logger)` in the `auth` config uses it.
236Leave the mailer unset and every link goes only to the log, which is a
237development convenience, not sign-in.
238
239(The manifest generator β€” `rastrillo new`, `manifest/*.toml`,
240`rastrillo generate` β€” is the optional declarative path: declare a
241resource once and its store, screens, and locale keys are generated;
242`scope = "user"` (v0.11.0+) owner-filters the generated queries for
243user-owned resources. Mix declared and hand-written resources freely.
244building-carlos-apps' `references/rastrillo.md` covers it.)
245
246`rastrillo.Resolve` + `Serve` speak the platform's process contract β€”
247your binary accepts `--socket <path> --db <path>` and serves
248`GET /healthz` and `GET /api/version`. **There is no `$PORT`**;
249instances listen on unix sockets the platform hands them. Do not
250hand-roll flag parsing.
251
252For the screens, use Rastrillo's `ui` partials and its attribute
253vocabulary (`<form rst-form>`, `rst-btn="primary"` β€” not classes), and
254no `style` attributes or `<style>` blocks: the baseline CSP drops them
255silently. Load
256its vendored `tokens.css` before a small app stylesheet; use that layer for
257app layout, token overrides and components Rastrillo does not provide.
258Read [the design-system guidance](../building-carlos-apps/references/rastrillo.md#design-system-and-app-css)
259before writing templates or CSS, and follow the docs for the app's installed
260Rastrillo version. Keep shared controls and their interaction states intact.
261
262### Put the code on amadan
263
264Load amadan's house rules for agents before the first command there:
265`amadan agent skill -install`, or read
266`https://amadan.net/docs/-/agent-skill.md`. They cover posting as the
267agent account (never the operator's), the session block every write
268carries, and the worktree-and-branch habit. Work as that account, with
269`AMADAN_HOME` pointing at the credentials the person minted.
270
271```sh
272amadan repo create <ns>/myapp -tier private # tiers: amadan.net/docs/tiers
273git init -b main
274git config user.name <name> # the agent's identity, before the first commit:
275git config user.email <name>~<operator keymail> # repo-local, so it never borrows the person's
276git add -A && git commit -m "Scaffold myapp"
277git remote add origin https://amadan.net/<ns>/myapp
278```
279
280**Then have the person start the runner, and only then push.** amadan
281queues a job only if a runner has already polled *that* repo: a runner
282started with `-repo <ns>/myapp` polls it from the start, and one without
283`-repo` picks a new repo up within about a minute. Push before that and
284the commit gets no job. If it happens, `amadan ci status` on the commit
285says "No runner serves this repo"; pushing the same commit again does
286nothing, so push a new commit once the runner is up.
287
288```sh
289git push -u origin main # after the runner is up
290amadan ci status <ns>/myapp # the job, or why there is none
291```
292
293git asks for a password on a private repo: give it the agent's token
294(the username is ignored) through a credential helper, never in the
295remote URL. That first push of the scaffold is the one commit straight
296to main.
297After it, every change starts with
298`amadan start <branch> -prompt - -summary "..."`: worktree, branch,
299your git identity, push and description in one step. On amadan the
300branch **is** the pull request; there is no separate step to open one.
301
302CI needs no config: the runner runs the scaffold's `.amadan/ci.d/`
303steps, the same gate as `make ci` (amadan.net/docs/ci).
304
305Deploying stays a command you run (Step 3). If the person wants CI to
306deploy, that is a `deploy`-role service credential (building-carlos-apps,
307platform.md) placed in the environment of a runner that serves only this
308repo: an amadan job sees its runner's environment (bar `AMADAN_*`), so a shared runner
309would hand the credential to every repo it serves.
310
311## Step 3 β€” first deploy
312
313Provision the instance (once), build for the boxes, deploy:
314
315```sh
316carlos instances enable --app myapp # opt-in; console pins your <sqid> domain
317carlos instances create --app myapp --host myapp.<sqid>.oncarlos.com --channel edge
318# substitute your real sqid (carlos auth whoami shows it) β€” the host is typed in full.
319# --channel is optional since 2026-08-31 (a first instance follows the app's pipeline
320# entry, not stable), and naming it explicitly is never wrong.
321
322GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build \
323 -ldflags "-X amadan.net/rastrillo/rastrillo.BuildVersion=$(git rev-parse --short HEAD)" \
324 -o myapp-linux-arm64 ./cmd/myapp
325
326carlos deploy --app myapp --message "Publish the first working app" ./myapp-linux-arm64
327```
328
329Agents pass `--message` on deploys and other mutations that support it. Describe
330what is being changed in one concise sentence; the Activity entry records the
331version and target separately. Do not wait for the interactive prompt during an
332automated run. See [Activity descriptions](../building-carlos-apps/references/platform.md#describe-changes-in-activity).
333
334The platform's boxes are **linux/arm64** β€” build exactly that, statically
335(`CGO_ENABLED=0`; cgo is why the framework uses `modernc.org/sqlite`).
336
337`carlos deploy` is ship + promote + watch in one motion: it uploads an
338immutable release, promotes it to your app's entry channel (`edge` β€”
339for a fresh app, edge is production; the old three-rung ladder is now
340an opt-in pipeline), then follows the rollout box by box (on Carloku;
341elsewhere it watches the URL) until the new build serves, and prints `live https://…`. Set `carlos apps build-probe
342--app myapp /api/version` once so it asks the process which build it
343runs rather than trusting the `X-Carlos-Version` header. On first
344run it offers to remember the artifact path in `.carlos/config`
345(commit that file); after that, releasing is just `carlos deploy`, no
346arguments.
347
348### The static path
349
350No instance, no binary β€” deploy the directory:
351
352```sh
353carlos deploy --app myapp --kind static --host myapp.<sqid>.oncarlos.com ./public
354```
355
356`--host` is required on the **first** static deploy and only that one: a
357static app has no instance record yet, so there is nothing to resolve the
358host and channel from. That deploy attaches the host and writes the
359record; afterwards `carlos deploy` from the project dir resolves both on
360its own. Omitting `--host` on a first deploy is a refusal, not a default.
361
362**Do not run `carlos instances enable` or `create` here.** "No instance"
363is literal: a static site is served by the edge straight off the channel
364pointer, with no process to start and nothing to wake. Step 3's recipe is
365for binaries, and following it for a site builds a route that succeeds at
366every step and then fails every wake. This one command is the whole
367deploy.
368
369## Step 4 β€” verify like the family does
370
371The deploy's own `live` line is the primary proof. To re-verify by hand:
372
373```sh
374curl -sI https://myapp.<sqid>.oncarlos.com | grep -i x-carlos-version
375carlos channels --app myapp # what each channel currently serves
376carlos logs --app myapp -f # merged app + platform timeline, no box access
377```
378
379Two traps, both paid for:
380
381- **A 200 is not a verification.** An app-shell route happily serves any
382 build ever shipped; only the `X-Carlos-Version` header (stamped after
383 the instance actually restarted) is proof. Verify against the thing you
384 changed, with the binary you built.
385- **Hibernation is the story, not a bug.** Idle instances doze; the first
386 request wakes them. A session in flight keeps the old binary until its
387 instance idles β€” that is the bake working, not a deploy that failed.
388
389## Iterating
390
391| Want | Command |
392|---|---|
393| Release again | `carlos deploy --message "Fix invoice rounding"` (from the project dir) |
394| Config var | `carlos env set --app myapp --message "Update the app setting" KEY=value` (converges in seconds) |
395| Secret | `carlos secrets set --app myapp KEY=value` (sealed, never printed) |
396| Tail logs | `carlos logs --app myapp -f` |
397| Bounce the process | `carlos restart --app myapp --message "Restart after changing configuration"` |
398| Undo a release | `carlos rollback --app myapp edge` |
399| List releases | `carlos releases --app myapp` |
400| Custom domain | `carlos domains attach --app myapp www.example.com` β€” it tells you the DNS records to create; certs are automatic once DNS points at the platform |
401| Review a branch | `carlos canary --app myapp ./myapp-linux-arm64` β€” a branch-only channel and URL that cannot disturb production. If the app carries config, pass `--environment`, or the canary host serves GETs and fails every form |
402| Who is visiting | `carlos analytics --app myapp --set-count edge`, then `--range 7d` β€” counted at the edge: no JavaScript, no cookie, no stored IP, so don't add an analytics script |
403| What is breaking | `carlos errors --app myapp --since 24h` β€” your `error` log lines, grouped, no wiring at all. The 502/503/504s the edge saw are `carlos availability --app myapp` |
404| Keep it private | `carlos gate set --app myapp` β€” one shared password in front of the hostname, for a prototype nobody should read yet |
405| Feature switch | `carlos features set --app myapp beta-ui=v2` β€” the platform serves it, only your app reads it, and it lands with no restart |
406
407Everything above is a console-mediated write that boxes converge within
408seconds. There is no restart-by-SSH, no cert ceremony, no Litestream
409config: routing, TLS, replication, restore drills, hibernation, and
410restarts are the platform's job. If you find yourself hand-rolling any
411of those, stop β€” you are rebuilding the platform under your app.
412
413## Conventions that still bind you
414
415The platform mechanized the infrastructure, not the discipline:
416
417- The gate (`make ci`) green before every commit, with `CGO_ENABLED=0`
418 builds throughout; add `rastrillo generate --check` to it only if the
419 app declares manifest resources.
420- Migrations are additive-only β€” new code over an old DB must always be
421 safe. Never delete data to update.
422- Zero-JS first; when JS is earned, small ES modules, no bundler, no
423 build step, 300-line cap.
424- Worktree per session, on a branch, with regular commits pushed as you
425 go. Merge to main only once the work is done, a human has
426 approved it, and CI is green (on amadan, `amadan branch merge`: a merge
427 commit, or one commit with `-squash`; never squash by hand, which
428 amadan shows as a deleted branch) β€” until the repo has CI there is no green
429 to wait for, so done + approved is the gate, and standing up CI (the
430 same gate as above, run for you) is early work rather than a
431 prerequisite nobody can meet. Deploy only what merged. Commit trailers
432 mark AI authorship (πŸ€– / `Co-Authored-By`).
433
434The full working conventions are building-carlos-apps'
435`references/process.md`.
436
437## Common mistakes
438
439| Mistake | Reality |
440|---|---|
441| Reading `$PORT` and calling `http.ListenAndServe` | The contract is `--socket <path> --db <path>` on a unix socket. `rastrillo.Resolve`+`Serve` handle it; hand-rolled servers must too. |
442| Building for the local machine | Boxes are linux/arm64. `GOOS=linux GOARCH=arm64 CGO_ENABLED=0`, always. |
443| A bare `First(&x, id)` on a user-owned row | Every such query goes through `scope.Owned` β€” reads, writes, and transactions alike. The isolation test suite in `examples/notes` is the regression guard to copy. |
444| Binding a form onto a GORM model | Mass assignment. Explicit `map[string]any` + `.Select` allowlist, per SKILL.md. |
445| Hand-rolling a router, certs, or Litestream | Platform's job. Your app is one binary on one socket. |
446| Verifying with a 200 or a cached DNS answer | Only `X-Carlos-Version` is proof, on the canonical host, after the deploy watch. |
447| Waiting for an SSH step that never comes | Every step is a `carlos` command. A step that needs box access is a wrong turn (or a product gap to file β€” not a workaround to build). |
448| Destructive migration "to clean up" | Additive-only, forever. |
449| Skipping the trade-offs line in the README | The trust default is legitimate only when declared. One dated line. |
450
451## When to leave this skill
452
453The moment the app needs a choice β€” end-to-end or partial encryption, a
454feed/real-time client shape, self-hosting, fleets of your own boxes β€” or a
455capability this recipe skips, such as sending email (`carlos email enable`)
456or work that has to happen on a clock (`carlos schedule set` β€” an idle
457instance cannot run its own timer), both in platform.md, or **a product
458where each customer gets their own instance** (a signup flow that
459creates instances, one domain split by path) β€” switch to **carlos:building-carlos-apps**. It starts with tenancy (what
460one instance is, the user models, the trust classes, and the benefits
461each earns), then holds the decision axes,
462the family evidence for each option, and the deeper references this
463recipe deliberately skips.
464