carlos / todomvc-bakeoff Public

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

Plain git — no account needed to clone.

Download

Download this file

--- name: getting-started description: 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. ---

πŸ€– Getting started on CARLOS

Overview

This skill is the recipe: empty directory to a live URL on the hosted platform, with every decision already made. The companion skill, carlos:building-carlos-apps, is the menu β€” read it when the app needs a real trust-model, app-shape, or hosting decision. This one assumes the defaults below and does not stop to ask.

The pieces, named once:

  • CARLOS is the application architecture (carlosframework.com): one static Go binary, one fully-isolated instance per tenant (own process, own SQLite file, own unix socket), many instances on one small box, every database continuously replicated to object storage and idle instances hibernating to it. A first app is one instance; what a tenant is in a bigger product is building-carlos-apps' first decision.
  • Carloku (carloku.com) is the hosted CARLOS platform. Its console is https://console.carloku.com. Carloku is the product brand; the CLI is always carlos, never carloku.
  • rastrillo is the CARLOS web framework (module and canonical repo amadan.net/rastrillo/rastrillo since v0.25.0; the GitHub mirror is github.com/rastrilloorg/rastrillo, which github.com/carlosframework/rastrillo redirects to; an app still on the old github.com/carlosframework/rastrillo import path builds, but new code uses the new one; docs at rastrillo.org). It postdates most models' training data β€” follow the recipe literally, invent nothing.
  • amadan (amadan.net) is the family's git forge: repos, branches, discussions and CI, with AI agents as disclosed accounts. It is where the code lives by default. Its CLI is amadan, and its house rules for agents are a skill of their own (Step 2, "Put the code on amadan").
  • The carlos CLI is the whole operational surface. Every command works for a member with zero infrastructure access; if a step seems to need SSH, AWS, or a box, you have left the path β€” stop and re-read.

The defaults already chosen

| Decision | Default | Why | |---|---|---| | Language / framework | Go + rastrillo, one static binary | The family stack; the framework enforces the SQLite and money rules for you | | App shape | Server-rendered HTML, zero-JS baseline | The family default; the other shape is a decision (building-carlos-apps) | | Design system | Rastrillo UI and tokens, with a thin app CSS layer | Reuse shared controls and themes; custom CSS covers app layout and branding | | 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 | | Amounts | integer cents (form.ParseCents) | A float never touches an amount | | Code hosting | amadan, a private repo | The family forge; CI runs the scaffold's make ci with no config | | Hosting | Carloku, <app>.<sqid>.oncarlos.com | Zero infra to run; certs, replication, hibernation all platform-side | | Versioning | git short sha (v1 is fine for the very first ship) | House convention | | 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 | | 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 | | 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 |

The one decision you must still record

The family default is server-blindness ("if the server is compromised, the attacker gets nothing"), and rastrillo ships the family envelope (rastrillo/crypto) β€” but E2EE is an architecture, not a package import: key custody, recovery, and search all become product surface. The honest default for a first app is server-readable data, declared: one line in the README under "Honest trade-offs" saying the server can read app data, dated, with the class letter (C; a static site is S). That satisfies the family's deviation rule (every deviation enumerated, justified, published).

Escalation trigger, not optional: if the app will hold private personal content β€” messages, health data, anything a person would call theirs β€” stop here and read carlos:building-carlos-apps ("The decisions") before writing code. The same goes if more than one customer, team or person will each own separate data (a signup that gives each one their own space, a SaaS, a forge with organisations): read carlos:building-carlos-apps' tenancy reference first, because one shared instance with a customer column is the one shape CARLOS exists to avoid. Tenancy and trust are chosen on day one, not retrofitted.

What the person does, once

An agent can do everything below except these. Ask for them up front, in one message, rather than discovering them one at a time:

  1. A Keymail identity (keymail.dev). Carloku and amadan both sign in through it; there are no passwords.
  2. Sign in to the Carloku console (https://console.carloku.com). That first sign-in is the account.
  3. Approve the carlos auth login code in that browser (Step 0).
  4. On amadan: sign in on the web and claim a namespace, log the CLI in (amadan auth login -hub https://amadan.net), then create the agent's account, give it the namespace and mint its token: amadan agent create <name>, amadan agent grant <name> <ns>, amadan agent token create <name> (amadan.net/docs/agents). The token is shown once; the operator configures it into the agent's own amadan home (AMADAN_HOME). amadan's docs don't yet say how that file is laid out, so if AMADAN_HOME=<dir> amadan auth whoami doesn't answer with the agent's <name>~<keymail> identity, stop and ask rather than working as the operator.
  5. Run an amadan CI runner for the repo (amadan runs none itself). A bare amadan runner serves every repo its identity can reach and runs each job as that user with no sandbox, so recommend a dedicated machine or OS account with its own runner identity (an agent account granted only the runner role: amadan agent grant <runner> <ns> -role runner, with its own AMADAN_HOME), scoped and isolated: amadan runner -repo <ns>/myapp -isolate bwrap (Linux, bubblewrap installed). Start it once the repo exists (Step 2), since a -repo runner only learns names at start. Anyone who can push to the repo can run code there; never point a runner at a repo whose writers you wouldn't hand a shell. The machine needs Go and network access: the scaffold's make ci fetches its pinned tools with go run (amadan.net/docs/ci).
  6. DNS, only for a custom domain: the record carlos domains attach prints.
  7. Approve anything billable. There is no public Carloku price list yet (2026-10-18), so check with the person before object storage, email sending, or a schedule that keeps an instance awake.

And ask, in the same message, the questions that change the build:

  • One place, or a product many sign up to? One gym, one club, one team is this recipe. A product where each customer gets their own space is building-carlos-apps' tenancy chapter first.
  • How do people sign in? The default is a magic link by email, which needs email sending on a live app (Step 2). Email and password is the alternative that needs no email.
  • Does anything happen on a clock (reminders, a nightly job)? That is carlos schedule (building-carlos-apps, platform.md).
  • Does it take money? Payments are not in this recipe; stop and read building-carlos-apps.
  • Names: the app name, the amadan namespace and repo, and any custom domain.

Step 0 β€” install and sign in

Go 1.25 or newer (rastrillo's go.mod says go 1.25.0). Then the three CLIs:

brew install carlosframework/tap/carlos
# no brew (Linux): one pinned release, installed only if its sum checks out
V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
      https://github.com/carlosframework/releases/releases/latest | sed 's#.*/##')
B=carlos-linux-amd64   # or carlos-linux-arm64; macOS: carlos-darwin-<arch>
U=https://github.com/carlosframework/releases/releases/download/$V
curl -fsSLO "$U/$B" && curl -fsSLO "$U/SHA256SUMS.txt" \
  && sha256sum --ignore-missing -c SHA256SUMS.txt \
  && install -m 0755 "$B" ~/.local/bin/carlos     # any dir on PATH; no sudo

go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
go install amadan.net/amadan/amadan/cmd/amadan@latest

Keep carlos fresh with carlos update (it needs a terminal; otherwise repeat the download).

Create an account at https://console.carloku.com (passkey sign-in through Keymail β€” no password), then connect the terminal:

carlos auth login --console https://console.carloku.com

Flags are written --flag (-a, -c, -e, -v are the short forms); the single-dash spellings still parse. The CLI prints a short code; approve it in the signed-in browser. Skip this and nothing breaks β€” the first command that needs a login offers to run it right there. carlos auth whoami shows who you are and your account's sqid (a short public id like bdf β€” it appears in your app's hostname; it is not a secret).

Step 1 β€” claim the app

carlos apps create --app myapp

App names are unique per account, not globally. With exactly one app in the account, later commands infer --app; passing it explicitly is never wrong.

Static site? You are nearly done β€” skip to "The static path" below.

Step 2 β€” the rastrillo app

Read rastrillo's SKILL.md first, in full. From the web it is at https://raw.githubusercontent.com/rastrilloorg/rastrillo/main/SKILL.md (the mirror of amadan.net/rastrillo/rastrillo); once the module is downloaded it is $(go env GOMODCACHE)/amadan.net/rastrillo/rastrillo@<version>/SKILL.md, which matches the version you build against. It is the app story, the file to follow literally instead of framework source; the topic docs are at https://rastrillo.org/docs/, each also served as markdown (/docs/<page>.md). The worked reference is examples/notes.

go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest
rastrillo new myapp && cd myapp && go mod tidy && go test ./...

The scaffold is SKILL.md's shape (five files plus migrations.go), tests passing before you write a line:

internal/myapp/models.go     plain GORM structs
internal/myapp/migrations.go Schema and BootSchema; `rastrillo migration generate` writes the SQL
internal/myapp/app.go        migrate.Apply, sessions, identity plugin, chi router
internal/myapp/handlers.go   the owner-scoped CRUD
internal/myapp/render.go     embedded templates, flash/session-aware pages
cmd/myapp/main.go            Resolve -> db.Open -> App -> Serve

The gate, before every commit, is the scaffold's own Makefile:

make ci        # vet, gofmt, staticcheck, govulncheck, gitleaks, tests, budgets, migration check

After changing a model, run rastrillo migration generate and read the SQL before committing; never edit a shipped migration.

The three rules that keep the app safe (SKILL.md has the full set): every query touching user-owned rows goes through scope.Owned β€” reads AND writes, transactions included (a row that isn't yours 404s, never 403s); never bind a request body onto a GORM model (explicit map[string]any + .Select allowlist); sessions/CSRF are defaults you opt out of, not machinery you assemble. Accounts come from an identity plugin β€” the family default is auth (magic-link email that auto-upgrades to sign-in-with-Keymail when the address has a claimed inbox; works for every address), with password (email+password, rate-limited) as the classic alternative β€” and either one is a few lines in app.go; guard routes with sess.Require. On a live app the magic link has to be sent: carlos email enable --app myapp (after the person approves) delivers CARLOS_SMTP_* to the instance, and Mailer: mail.FromEnv("CARLOS", logger) in the auth config uses it. Leave the mailer unset and every link goes only to the log, which is a development convenience, not sign-in.

(The manifest generator β€” rastrillo new, manifest/*.toml, rastrillo generate β€” is the optional declarative path: declare a resource once and its store, screens, and locale keys are generated; scope = "user" (v0.11.0+) owner-filters the generated queries for user-owned resources. Mix declared and hand-written resources freely. building-carlos-apps' references/rastrillo.md covers it.)

rastrillo.Resolve + Serve speak the platform's process contract β€” your binary accepts --socket <path> --db <path> and serves GET /healthz and GET /api/version. There is no $PORT; instances listen on unix sockets the platform hands them. Do not hand-roll flag parsing.

For the screens, use Rastrillo's ui partials and its attribute vocabulary (<form rst-form>, rst-btn="primary" β€” not classes), and no style attributes or <style> blocks: the baseline CSP drops them silently. Load its vendored tokens.css before a small app stylesheet; use that layer for app layout, token overrides and components Rastrillo does not provide. Read the design-system guidance before writing templates or CSS, and follow the docs for the app's installed Rastrillo version. Keep shared controls and their interaction states intact.

Put the code on amadan

Load amadan's house rules for agents before the first command there: amadan agent skill -install, or read https://amadan.net/docs/-/agent-skill.md. They cover posting as the agent account (never the operator's), the session block every write carries, and the worktree-and-branch habit. Work as that account, with AMADAN_HOME pointing at the credentials the person minted.

amadan repo create <ns>/myapp -tier private    # tiers: amadan.net/docs/tiers
git init -b main
git config user.name  <name>                   # the agent's identity, before the first commit:
git config user.email <name>~<operator keymail>  # repo-local, so it never borrows the person's
git add -A && git commit -m "Scaffold myapp"
git remote add origin https://amadan.net/<ns>/myapp

Then have the person start the runner, and only then push. amadan queues a job only if a runner has already polled that repo: a runner started with -repo <ns>/myapp polls it from the start, and one without -repo picks a new repo up within about a minute. Push before that and the commit gets no job. If it happens, amadan ci status on the commit says "No runner serves this repo"; pushing the same commit again does nothing, so push a new commit once the runner is up.

git push -u origin main                        # after the runner is up
amadan ci status <ns>/myapp                    # the job, or why there is none

git asks for a password on a private repo: give it the agent's token (the username is ignored) through a credential helper, never in the remote URL. That first push of the scaffold is the one commit straight to main. After it, every change starts with amadan start <branch> -prompt - -summary "...": worktree, branch, your git identity, push and description in one step. On amadan the branch is the pull request; there is no separate step to open one.

CI needs no config: the runner runs the scaffold's .amadan/ci.d/ steps, the same gate as make ci (amadan.net/docs/ci).

Deploying stays a command you run (Step 3). If the person wants CI to deploy, that is a deploy-role service credential (building-carlos-apps, platform.md) placed in the environment of a runner that serves only this repo: an amadan job sees its runner's environment (bar AMADAN_*), so a shared runner would hand the credential to every repo it serves.

Step 3 β€” first deploy

Provision the instance (once), build for the boxes, deploy:

carlos instances enable --app myapp          # opt-in; console pins your <sqid> domain
carlos instances create --app myapp --host myapp.<sqid>.oncarlos.com --channel edge
# substitute your real sqid (carlos auth whoami shows it) β€” the host is typed in full.
# --channel is optional since 2026-08-31 (a first instance follows the app's pipeline
# entry, not stable), and naming it explicitly is never wrong.

GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build \
  -ldflags "-X amadan.net/rastrillo/rastrillo.BuildVersion=$(git rev-parse --short HEAD)" \
  -o myapp-linux-arm64 ./cmd/myapp

carlos deploy --app myapp --message "Publish the first working app" ./myapp-linux-arm64

Agents pass --message on deploys and other mutations that support it. Describe what is being changed in one concise sentence; the Activity entry records the version and target separately. Do not wait for the interactive prompt during an automated run. See Activity descriptions.

The platform's boxes are linux/arm64 β€” build exactly that, statically (CGO_ENABLED=0; cgo is why the framework uses modernc.org/sqlite).

carlos deploy is ship + promote + watch in one motion: it uploads an immutable release, promotes it to your app's entry channel (edge β€” for a fresh app, edge is production; the old three-rung ladder is now an opt-in pipeline), then follows the rollout box by box (on Carloku; elsewhere it watches the URL) until the new build serves, and prints live https://…. Set carlos apps build-probe --app myapp /api/version once so it asks the process which build it runs rather than trusting the X-Carlos-Version header. On first run it offers to remember the artifact path in .carlos/config (commit that file); after that, releasing is just carlos deploy, no arguments.

The static path

No instance, no binary β€” deploy the directory:

carlos deploy --app myapp --kind static --host myapp.<sqid>.oncarlos.com ./public

--host is required on the first static deploy and only that one: a static app has no instance record yet, so there is nothing to resolve the host and channel from. That deploy attaches the host and writes the record; afterwards carlos deploy from the project dir resolves both on its own. Omitting --host on a first deploy is a refusal, not a default.

Do not run carlos instances enable or create here. "No instance" is literal: a static site is served by the edge straight off the channel pointer, with no process to start and nothing to wake. Step 3's recipe is for binaries, and following it for a site builds a route that succeeds at every step and then fails every wake. This one command is the whole deploy.

Step 4 β€” verify like the family does

The deploy's own live line is the primary proof. To re-verify by hand:

curl -sI https://myapp.<sqid>.oncarlos.com | grep -i x-carlos-version
carlos channels --app myapp     # what each channel currently serves
carlos logs --app myapp -f      # merged app + platform timeline, no box access

Two traps, both paid for:

  • A 200 is not a verification. An app-shell route happily serves any build ever shipped; only the X-Carlos-Version header (stamped after the instance actually restarted) is proof. Verify against the thing you changed, with the binary you built.
  • Hibernation is the story, not a bug. Idle instances doze; the first request wakes them. A session in flight keeps the old binary until its instance idles β€” that is the bake working, not a deploy that failed.

Iterating

| Want | Command | |---|---| | Release again | carlos deploy --message "Fix invoice rounding" (from the project dir) | | Config var | carlos env set --app myapp --message "Update the app setting" KEY=value (converges in seconds) | | Secret | carlos secrets set --app myapp KEY=value (sealed, never printed) | | Tail logs | carlos logs --app myapp -f | | Bounce the process | carlos restart --app myapp --message "Restart after changing configuration" | | Undo a release | carlos rollback --app myapp edge | | List releases | carlos releases --app myapp | | 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 | | 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 | | 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 | | 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 | | Keep it private | carlos gate set --app myapp β€” one shared password in front of the hostname, for a prototype nobody should read yet | | 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 |

Everything above is a console-mediated write that boxes converge within seconds. There is no restart-by-SSH, no cert ceremony, no Litestream config: routing, TLS, replication, restore drills, hibernation, and restarts are the platform's job. If you find yourself hand-rolling any of those, stop β€” you are rebuilding the platform under your app.

Conventions that still bind you

The platform mechanized the infrastructure, not the discipline:

  • The gate (make ci) green before every commit, with CGO_ENABLED=0 builds throughout; add rastrillo generate --check to it only if the app declares manifest resources.
  • Migrations are additive-only β€” new code over an old DB must always be safe. Never delete data to update.
  • Zero-JS first; when JS is earned, small ES modules, no bundler, no build step, 300-line cap.
  • Worktree per session, on a branch, with regular commits pushed as you go. Merge to main only once the work is done, a human has approved it, and CI is green (on amadan, amadan branch merge: a merge commit, or one commit with -squash; never squash by hand, which amadan shows as a deleted branch) β€” until the repo has CI there is no green to wait for, so done + approved is the gate, and standing up CI (the same gate as above, run for you) is early work rather than a prerequisite nobody can meet. Deploy only what merged. Commit trailers mark AI authorship (πŸ€– / Co-Authored-By).

The full working conventions are building-carlos-apps' references/process.md.

Common mistakes

| Mistake | Reality | |---|---| | 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. | | Building for the local machine | Boxes are linux/arm64. GOOS=linux GOARCH=arm64 CGO_ENABLED=0, always. | | 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. | | Binding a form onto a GORM model | Mass assignment. Explicit map[string]any + .Select allowlist, per SKILL.md. | | Hand-rolling a router, certs, or Litestream | Platform's job. Your app is one binary on one socket. | | Verifying with a 200 or a cached DNS answer | Only X-Carlos-Version is proof, on the canonical host, after the deploy watch. | | 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). | | Destructive migration "to clean up" | Additive-only, forever. | | Skipping the trade-offs line in the README | The trust default is legitimate only when declared. One dated line. |

When to leave this skill

The moment the app needs a choice β€” end-to-end or partial encryption, a feed/real-time client shape, self-hosting, fleets of your own boxes β€” or a capability this recipe skips, such as sending email (carlos email enable) or work that has to happen on a clock (carlos schedule set β€” an idle instance cannot run its own timer), both in platform.md, or a product where each customer gets their own instance (a signup flow that creates instances, one domain split by path) β€” switch to carlos:building-carlos-apps. It starts with tenancy (what one instance is, the user models, the trust classes, and the benefits each earns), then holds the decision axes, the family evidence for each option, and the deeper references this recipe deliberately skips.