| 1 | --- |
| 2 | name: getting-started |
| 3 | 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. |
| 4 | --- |
| 5 | |
| 6 | # π€ Getting started on CARLOS |
| 7 | |
| 8 | ## Overview |
| 9 | |
| 10 | This skill is the recipe: empty directory to a live URL on the hosted |
| 11 | platform, with every decision already made. The companion skill, |
| 12 | **carlos:building-carlos-apps**, is the menu β read it when the app needs |
| 13 | a real trust-model, app-shape, or hosting decision. This one assumes the |
| 14 | defaults below and does not stop to ask. |
| 15 | |
| 16 | The 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 | |
| 60 | The family default is server-blindness ("if the server is compromised, |
| 61 | the attacker gets nothing"), and rastrillo ships the family |
| 62 | envelope (`rastrillo/crypto`) β but E2EE is an architecture, not a |
| 63 | package import: key custody, recovery, and search all become product |
| 64 | surface. The honest default for a first app is |
| 65 | **server-readable data, declared**: one line |
| 66 | in the README under "Honest trade-offs" saying the server can read app |
| 67 | data, dated, with the class letter (C; a static site is S). That |
| 68 | satisfies the family's deviation rule (every deviation enumerated, |
| 69 | justified, published). |
| 70 | |
| 71 | **Escalation trigger, not optional:** if the app will hold private |
| 72 | personal content β messages, health data, anything a person would call |
| 73 | theirs β stop here and read carlos:building-carlos-apps ("The |
| 74 | decisions") before writing code. The same goes if **more than one |
| 75 | customer, team or person will each own separate data** (a signup that |
| 76 | gives each one their own space, a SaaS, a forge with organisations): |
| 77 | read carlos:building-carlos-apps' tenancy reference first, because one |
| 78 | shared instance with a customer column is the one shape CARLOS exists to |
| 79 | avoid. Tenancy and trust are chosen on day one, not retrofitted. |
| 80 | |
| 81 | ## What the person does, once |
| 82 | |
| 83 | An agent can do everything below except these. Ask for them up front, in |
| 84 | one message, rather than discovering them one at a time: |
| 85 | |
| 86 | 1. **A Keymail identity** (keymail.dev). Carloku and amadan both sign in |
| 87 | through it; there are no passwords. |
| 88 | 2. **Sign in to the Carloku console** (`https://console.carloku.com`). |
| 89 | That first sign-in is the account. |
| 90 | 3. **Approve the `carlos auth login` code** in that browser (Step 0). |
| 91 | 4. **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. |
| 101 | 5. **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). |
| 113 | 6. **DNS**, only for a custom domain: the record `carlos domains attach` |
| 114 | prints. |
| 115 | 7. **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 | |
| 119 | And 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 | |
| 136 | Go **1.25 or newer** (rastrillo's `go.mod` says `go 1.25.0`). Then the |
| 137 | three CLIs: |
| 138 | |
| 139 | ```sh |
| 140 | brew install carlosframework/tap/carlos |
| 141 | # no brew (Linux): one pinned release, installed only if its sum checks out |
| 142 | V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \ |
| 143 | https://github.com/carlosframework/releases/releases/latest | sed 's#.*/##') |
| 144 | B=carlos-linux-amd64 # or carlos-linux-arm64; macOS: carlos-darwin-<arch> |
| 145 | U=https://github.com/carlosframework/releases/releases/download/$V |
| 146 | curl -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 | |
| 150 | go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest |
| 151 | go install amadan.net/amadan/amadan/cmd/amadan@latest |
| 152 | ``` |
| 153 | |
| 154 | Keep `carlos` fresh with `carlos update` (it needs a terminal; otherwise |
| 155 | repeat the download). |
| 156 | |
| 157 | Create an account at `https://console.carloku.com` (passkey sign-in |
| 158 | through Keymail β no password), then connect the terminal: |
| 159 | |
| 160 | ```sh |
| 161 | carlos auth login --console https://console.carloku.com |
| 162 | ``` |
| 163 | |
| 164 | Flags are written `--flag` (`-a`, `-c`, `-e`, `-v` are the short forms); |
| 165 | the single-dash spellings still parse. The CLI prints a short code; |
| 166 | approve it in the signed-in browser. Skip |
| 167 | this and nothing breaks β the first command that needs a login offers to |
| 168 | run it right there. `carlos auth whoami` shows who you are and your |
| 169 | account's **sqid** (a short public id like `bdf` β it appears in your |
| 170 | app's hostname; it is not a secret). |
| 171 | |
| 172 | ## Step 1 β claim the app |
| 173 | |
| 174 | ```sh |
| 175 | carlos apps create --app myapp |
| 176 | ``` |
| 177 | |
| 178 | App names are unique per account, not globally. With exactly one app in |
| 179 | the account, later commands infer `--app`; passing it explicitly is never |
| 180 | wrong. |
| 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 |
| 189 | downloaded it is |
| 190 | `$(go env GOMODCACHE)/amadan.net/rastrillo/rastrillo@<version>/SKILL.md`, |
| 191 | which matches the version you build against. It is the app story, the |
| 192 | file to follow literally instead of framework source; the topic docs |
| 193 | are at `https://rastrillo.org/docs/`, each also served as markdown |
| 194 | (`/docs/<page>.md`). The worked reference is `examples/notes`. |
| 195 | |
| 196 | ```sh |
| 197 | go install amadan.net/rastrillo/rastrillo/cmd/rastrillo@latest |
| 198 | rastrillo new myapp && cd myapp && go mod tidy && go test ./... |
| 199 | ``` |
| 200 | |
| 201 | The scaffold is SKILL.md's shape (five files plus `migrations.go`), |
| 202 | tests passing before you write a line: |
| 203 | |
| 204 | ``` |
| 205 | internal/myapp/models.go plain GORM structs |
| 206 | internal/myapp/migrations.go Schema and BootSchema; `rastrillo migration generate` writes the SQL |
| 207 | internal/myapp/app.go migrate.Apply, sessions, identity plugin, chi router |
| 208 | internal/myapp/handlers.go the owner-scoped CRUD |
| 209 | internal/myapp/render.go embedded templates, flash/session-aware pages |
| 210 | cmd/myapp/main.go Resolve -> db.Open -> App -> Serve |
| 211 | ``` |
| 212 | |
| 213 | The gate, before every commit, is the scaffold's own `Makefile`: |
| 214 | |
| 215 | ```sh |
| 216 | make ci # vet, gofmt, staticcheck, govulncheck, gitleaks, tests, budgets, migration check |
| 217 | ``` |
| 218 | |
| 219 | After changing a model, run `rastrillo migration generate` and read the |
| 220 | SQL before committing; never edit a shipped migration. |
| 221 | |
| 222 | The three rules that keep the app safe (SKILL.md has the full set): |
| 223 | every query touching user-owned rows goes through `scope.Owned` β |
| 224 | reads AND writes, transactions included (a row that isn't yours 404s, |
| 225 | never 403s); never bind a request body onto a GORM model (explicit |
| 226 | `map[string]any` + `.Select` allowlist); sessions/CSRF are defaults |
| 227 | you opt out of, not machinery you assemble. Accounts come from an |
| 228 | identity plugin β the family default is `auth` (magic-link email that |
| 229 | auto-upgrades to sign-in-with-Keymail when the address has a claimed |
| 230 | inbox; works for every address), with `password` (email+password, |
| 231 | rate-limited) as the classic alternative β and either one is a few |
| 232 | lines in `app.go`; guard routes with `sess.Require`. **On a live app the |
| 233 | magic link has to be sent**: `carlos email enable --app myapp` (after |
| 234 | the person approves) delivers `CARLOS_SMTP_*` to the instance, and |
| 235 | `Mailer: mail.FromEnv("CARLOS", logger)` in the `auth` config uses it. |
| 236 | Leave the mailer unset and every link goes only to the log, which is a |
| 237 | development convenience, not sign-in. |
| 238 | |
| 239 | (The manifest generator β `rastrillo new`, `manifest/*.toml`, |
| 240 | `rastrillo generate` β is the optional declarative path: declare a |
| 241 | resource once and its store, screens, and locale keys are generated; |
| 242 | `scope = "user"` (v0.11.0+) owner-filters the generated queries for |
| 243 | user-owned resources. Mix declared and hand-written resources freely. |
| 244 | building-carlos-apps' `references/rastrillo.md` covers it.) |
| 245 | |
| 246 | `rastrillo.Resolve` + `Serve` speak the platform's process contract β |
| 247 | your binary accepts `--socket <path> --db <path>` and serves |
| 248 | `GET /healthz` and `GET /api/version`. **There is no `$PORT`**; |
| 249 | instances listen on unix sockets the platform hands them. Do not |
| 250 | hand-roll flag parsing. |
| 251 | |
| 252 | For the screens, use Rastrillo's `ui` partials and its attribute |
| 253 | vocabulary (`<form rst-form>`, `rst-btn="primary"` β not classes), and |
| 254 | no `style` attributes or `<style>` blocks: the baseline CSP drops them |
| 255 | silently. Load |
| 256 | its vendored `tokens.css` before a small app stylesheet; use that layer for |
| 257 | app layout, token overrides and components Rastrillo does not provide. |
| 258 | Read [the design-system guidance](../building-carlos-apps/references/rastrillo.md#design-system-and-app-css) |
| 259 | before writing templates or CSS, and follow the docs for the app's installed |
| 260 | Rastrillo version. Keep shared controls and their interaction states intact. |
| 261 | |
| 262 | ### Put the code on amadan |
| 263 | |
| 264 | Load 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 |
| 267 | agent account (never the operator's), the session block every write |
| 268 | carries, and the worktree-and-branch habit. Work as that account, with |
| 269 | `AMADAN_HOME` pointing at the credentials the person minted. |
| 270 | |
| 271 | ```sh |
| 272 | amadan repo create <ns>/myapp -tier private # tiers: amadan.net/docs/tiers |
| 273 | git init -b main |
| 274 | git config user.name <name> # the agent's identity, before the first commit: |
| 275 | git config user.email <name>~<operator keymail> # repo-local, so it never borrows the person's |
| 276 | git add -A && git commit -m "Scaffold myapp" |
| 277 | git remote add origin https://amadan.net/<ns>/myapp |
| 278 | ``` |
| 279 | |
| 280 | **Then have the person start the runner, and only then push.** amadan |
| 281 | queues a job only if a runner has already polled *that* repo: a runner |
| 282 | started 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 |
| 284 | the commit gets no job. If it happens, `amadan ci status` on the commit |
| 285 | says "No runner serves this repo"; pushing the same commit again does |
| 286 | nothing, so push a new commit once the runner is up. |
| 287 | |
| 288 | ```sh |
| 289 | git push -u origin main # after the runner is up |
| 290 | amadan ci status <ns>/myapp # the job, or why there is none |
| 291 | ``` |
| 292 | |
| 293 | git 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 |
| 295 | remote URL. That first push of the scaffold is the one commit straight |
| 296 | to main. |
| 297 | After it, every change starts with |
| 298 | `amadan start <branch> -prompt - -summary "..."`: worktree, branch, |
| 299 | your git identity, push and description in one step. On amadan the |
| 300 | branch **is** the pull request; there is no separate step to open one. |
| 301 | |
| 302 | CI needs no config: the runner runs the scaffold's `.amadan/ci.d/` |
| 303 | steps, the same gate as `make ci` (amadan.net/docs/ci). |
| 304 | |
| 305 | Deploying stays a command you run (Step 3). If the person wants CI to |
| 306 | deploy, that is a `deploy`-role service credential (building-carlos-apps, |
| 307 | platform.md) placed in the environment of a runner that serves only this |
| 308 | repo: an amadan job sees its runner's environment (bar `AMADAN_*`), so a shared runner |
| 309 | would hand the credential to every repo it serves. |
| 310 | |
| 311 | ## Step 3 β first deploy |
| 312 | |
| 313 | Provision the instance (once), build for the boxes, deploy: |
| 314 | |
| 315 | ```sh |
| 316 | carlos instances enable --app myapp # opt-in; console pins your <sqid> domain |
| 317 | carlos 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 | |
| 322 | GOOS=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 | |
| 326 | carlos deploy --app myapp --message "Publish the first working app" ./myapp-linux-arm64 |
| 327 | ``` |
| 328 | |
| 329 | Agents pass `--message` on deploys and other mutations that support it. Describe |
| 330 | what is being changed in one concise sentence; the Activity entry records the |
| 331 | version and target separately. Do not wait for the interactive prompt during an |
| 332 | automated run. See [Activity descriptions](../building-carlos-apps/references/platform.md#describe-changes-in-activity). |
| 333 | |
| 334 | The 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 |
| 338 | immutable release, promotes it to your app's entry channel (`edge` β |
| 339 | for a fresh app, edge is production; the old three-rung ladder is now |
| 340 | an opt-in pipeline), then follows the rollout box by box (on Carloku; |
| 341 | elsewhere 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 |
| 343 | runs rather than trusting the `X-Carlos-Version` header. On first |
| 344 | run it offers to remember the artifact path in `.carlos/config` |
| 345 | (commit that file); after that, releasing is just `carlos deploy`, no |
| 346 | arguments. |
| 347 | |
| 348 | ### The static path |
| 349 | |
| 350 | No instance, no binary β deploy the directory: |
| 351 | |
| 352 | ```sh |
| 353 | carlos 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 |
| 357 | static app has no instance record yet, so there is nothing to resolve the |
| 358 | host and channel from. That deploy attaches the host and writes the |
| 359 | record; afterwards `carlos deploy` from the project dir resolves both on |
| 360 | its 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" |
| 363 | is literal: a static site is served by the edge straight off the channel |
| 364 | pointer, with no process to start and nothing to wake. Step 3's recipe is |
| 365 | for binaries, and following it for a site builds a route that succeeds at |
| 366 | every step and then fails every wake. This one command is the whole |
| 367 | deploy. |
| 368 | |
| 369 | ## Step 4 β verify like the family does |
| 370 | |
| 371 | The deploy's own `live` line is the primary proof. To re-verify by hand: |
| 372 | |
| 373 | ```sh |
| 374 | curl -sI https://myapp.<sqid>.oncarlos.com | grep -i x-carlos-version |
| 375 | carlos channels --app myapp # what each channel currently serves |
| 376 | carlos logs --app myapp -f # merged app + platform timeline, no box access |
| 377 | ``` |
| 378 | |
| 379 | Two 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 | |
| 407 | Everything above is a console-mediated write that boxes converge within |
| 408 | seconds. There is no restart-by-SSH, no cert ceremony, no Litestream |
| 409 | config: routing, TLS, replication, restore drills, hibernation, and |
| 410 | restarts are the platform's job. If you find yourself hand-rolling any |
| 411 | of those, stop β you are rebuilding the platform under your app. |
| 412 | |
| 413 | ## Conventions that still bind you |
| 414 | |
| 415 | The 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 | |
| 434 | The 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 | |
| 453 | The moment the app needs a choice β end-to-end or partial encryption, a |
| 454 | feed/real-time client shape, self-hosting, fleets of your own boxes β or a |
| 455 | capability this recipe skips, such as sending email (`carlos email enable`) |
| 456 | or work that has to happen on a clock (`carlos schedule set` β an idle |
| 457 | instance cannot run its own timer), both in platform.md, or **a product |
| 458 | where each customer gets their own instance** (a signup flow that |
| 459 | creates instances, one domain split by path) β switch to **carlos:building-carlos-apps**. It starts with tenancy (what |
| 460 | one instance is, the user models, the trust classes, and the benefits |
| 461 | each earns), then holds the decision axes, |
| 462 | the family evidence for each option, and the deeper references this |
| 463 | recipe deliberately skips. |
| 464 | |