| 1 | # calls |
| 2 | |
| 3 | A [rastrillo](https://github.com/rastrilloorg/rastrillo) app. Read the |
| 4 | framework's own SKILL.md (repo root, or in the module cache) before |
| 5 | writing app code — it is the whole app story in ~15KB. The conventions |
| 6 | below are load-bearing; the framework enforces most of them |
| 7 | mechanically. |
| 8 | |
| 9 | ## Layout |
| 10 | |
| 11 | - `internal/<app>/` — the app: `models.go` (plain GORM structs |
| 12 | + Models), `migrations.go` + `migrations/` (Schema, BootSchema), |
| 13 | `app.go` (migrate.Apply + chi router), `handlers.go`, |
| 14 | `render.go` + `templates/`. This is SKILL.md's five-file shape. |
| 15 | - `manifest/` — the declarative path, optional and equal: TOML or .go |
| 16 | Resource manifests, each generating a full screen set under `gen/` |
| 17 | (see manifest/README.md for the mounting recipe). A hand-written |
| 18 | action file at a generated path (carrying `//go:build rastrillo_actions`) |
| 19 | takes that one screen over, by existence. |
| 20 | - `gen/` — generated when manifests are used; never edit. Regenerate |
| 21 | with `rastrillo generate`. |
| 22 | - `internal/calls/seal/` — the app's only encryption surface. A title |
| 23 | is sealed by the store and opened by the store; no handler holds a |
| 24 | plaintext title it did not get from `store.title`. `pending_joins` is |
| 25 | the fingerprinted table: an outsider's address is stored there only as |
| 26 | a keyed fingerprint, while `persons.email` holds a verified address in |
| 27 | plaintext, the suite's current shape for a person (spec §2.1, as in |
| 28 | Docs, Slides and Calendar). |
| 29 | Meeting invitation tokens are looked up by hash and sealed for repeat sharing |
| 30 | by members. Legacy hash-only invitations remain valid; first sharing creates |
| 31 | a sealed invitation alongside them. Replacing a link revokes all current ones. |
| 32 | - `internal/calls/store.go` — every database access. `standing.go` is |
| 33 | the entry authority (spec §2.3), computed per request, never stored. |
| 34 | - Sign-in: `auth.go` is Docs' shape with the pending-join exception of |
| 35 | spec §3.2 and Slides' same-answer wrapper. Keymail or an emailed link |
| 36 | is chosen by the address's domain alone, never by membership (Paul, |
| 37 | 2026-10-06). A Keymail address, member or not, gets the continuation |
| 38 | page to Keymail (never a 303 out of the form POST: Safari applies |
| 39 | `form-action` to it); every other answer from `POST /signin` is |
| 40 | `303 /signin?sent=1`, and the link is mailed after the response by |
| 41 | `signin_mail.go`'s worker, only if this request may have one, so |
| 42 | neither the answer nor its timing says who is a member. A refusal |
| 43 | after verification is the door, rendered in the refusal's own 403 by |
| 44 | auth's `Refused` hook (never a query anyone could type). An emailed |
| 45 | link opens the confirm door (`RenderConfirm`): `GET /auth/verify` |
| 46 | spends nothing, so a mail scanner cannot burn it, and the door's one |
| 47 | Sign in button POSTs the token back to `/auth/verify`, mounted for |
| 48 | both methods in both modes. `mayRequestLink` does the same |
| 49 | database work for a member and an outsider and logs nothing on the |
| 50 | response path. |
| 51 | |
| 52 | ## Rules the family holds hard |
| 53 | |
| 54 | - Every query touching user-owned rows goes through `scope.Owned` — |
| 55 | reads AND writes, transactions included. A row that isn't yours 404s, |
| 56 | never 403s. |
| 57 | - Never bind a request body onto a GORM model: explicit |
| 58 | `map[string]any` + `.Select` allowlist. |
| 59 | - Migrations apply at boot, once each, and are never re-run or reversed. |
| 60 | - Schema changes: edit models.go, then run `rastrillo migration generate` |
| 61 | and read the SQL it writes — it may be a full table rebuild, not just |
| 62 | an `ADD COLUMN`. Never edit a migration that has shipped. |
| 63 | Renames are hand-written (`rastrillo migration new rename_x`) because |
| 64 | a rename is indistinguishable from a drop plus an add. |
| 65 | - Money is integer cents. A float never touches a value a person will |
| 66 | be held to. |
| 67 | - Screens work with JavaScript disabled, with two exceptions: browser |
| 68 | media participation (the holding screen's camera preview and join |
| 69 | button, and the call itself in the room), and, once a call exists, |
| 70 | the room's chat panel and its admission queue's Admit controls; both |
| 71 | pages still render their title, standing, floor notice and |
| 72 | navigation without script. Destructive actions get their own |
| 73 | confirm-page URL. |
| 74 | - The gate is `make ci` (vet + gofmt + tests) — the same definition |
| 75 | CI runs. Run it before every push. `CGO_ENABLED=0` throughout: the |
| 76 | stack is cgo-free by design. |
| 77 | - Native Apple bindings under `native/` use cgo to bridge Go to Objective-C. |
| 78 | Their toolchain is isolated in `native/go.mod`; server releases remain |
| 79 | `CGO_ENABLED=0`. Build with `make native-mac`. |
| 80 | - `make race` is the test exception to that rule, and the third gate: |
| 81 | `-race` refuses to build without cgo, so it sets `CGO_ENABLED=1`. It |
| 82 | builds a test binary and never a release, so nothing that ships gains |
| 83 | a C dependency. Run it for any change to `internal/calls/hub` or the |
| 84 | stream — a hub behind a mutex, a tick goroutine and one goroutine per |
| 85 | stream is more concurrency than `make ci` can see. |
| 86 | - `make browser` is the second gate, for any change to a page or to the |
| 87 | client: a real call in real Chromium. It needs the Claude Code bash |
| 88 | sandbox **off** — the sandbox blocks UDP, so Chromium gathers no ICE |
| 89 | candidates and every call fails with `frames` stuck at 0, which reads |
| 90 | exactly like a client bug and is not one. |
| 91 | |
| 92 | ## Serving |
| 93 | |
| 94 | `rastrillo.Resolve` + `Serve` speak the CARLOS activation contract |
| 95 | (socket/addr/db argv, LISTEN_FDS, $STATE_DIRECTORY, `sidecar run`) — |
| 96 | the golden deployment target, not a requirement: `./app -addr :8080` |
| 97 | serves anywhere. `GET /healthz` and `GET /api/version` are answered |
| 98 | by the framework, outside app middleware. |
| 99 | |
| 100 | ## Deploying |
| 101 | |
| 102 | `make release VERSION=<sha>` builds and stamps the artifact; `carlos |
| 103 | deploy --app calls --account oficina --channel edge --version <sha> |
| 104 | --target linux-arm64 releases/calls-linux-arm64` ships and promotes it, |
| 105 | waiting until `/api/version` serves it — but only once `carlos apps |
| 106 | build-probe --app calls --account oficina /api/version` has been run |
| 107 | for the app (set on the flagship app since 2026-09-09); a new app or |
| 108 | account needs that first, or the wait watches `X-Carlos-Version` |
| 109 | instead, which can report the version adopted before the process has |
| 110 | actually changed. A migration applied under an earlier filename fails |
| 111 | at boot; otherwise migrations never re-run. Every deploy, and every |
| 112 | relay config change, restarts the instance and drops live calls — no |
| 113 | drain exists. The wait can time out before |
| 114 | hand-over — check `/api/version` before retrying. See the README's |
| 115 | Deploying section for the canary recipe. |
| 116 | |
| 117 | ## UX conventions (seeded from profile: considered) |
| 118 | |
| 119 | Accessible by construction, progressively enhanced, and never dependent on JavaScript to be usable, with the exceptions named above: browser media participation, on the holding screen and in the room, and, once a call exists, the room's chat panel and admission controls. |
| 120 | |
| 121 | These are rastrillo's own conventions, and the profile is named for what it does rather than after anyone else's work. For wider reading on interface quality, impeccable.style, the WAI-ARIA Authoring Practices and Inclusive Components are all worth your time — none of them is endorsed by, or authoritative for, this framework. |
| 122 | |
| 123 | - [x] Icons — lucide, inline — `internal/<app>/icons` |
| 124 | - [x] Fields — label, hint, help and error in one envelope, wired with aria-describedby — `ui/field, ui/field-select, ui/field-text` |
| 125 | - [x] Destructive actions — their own confirm-page URL, never a modal — `ui/confirm-form` |
| 126 | - [x] State — never colour alone — always a text label too — `ui/status-pill, ui/badge` |
| 127 | - [ ] Dates — relative, with the absolute date in a title attribute |
| 128 | - [ ] Errors — server-rendered beside the field, in plain language |
| 129 | - [x] Empty states — say what the screen is for, not just that it is empty — `ui/empty-state` |
| 130 | |
| 131 | `[x]` is enforced by a vendored component: use it, do not hand-roll an |
| 132 | equivalent. `[ ]` is guidance to apply by hand. A convention absent from |
| 133 | this list is one rastrillo has no opinion about. |
| 134 | |
| 135 | Edit any line. **This file is the source of truth, not the profile name.** |
| 136 | The profile only seeded this list at scaffold time; nothing re-reads it, so |
| 137 | changing a line here changes what the app does. A convention deleted from |
| 138 | this file is not a convention to reinstate. |
| 139 | |
| 140 | ## Work in a worktree, and catch main up first |
| 141 | |
| 142 | Take a worktree for the work. Do not work in the shared checkout. |
| 143 | |
| 144 | git -C <repo> fetch origin |
| 145 | git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main |
| 146 | |
| 147 | Both halves matter, for different reasons. |
| 148 | |
| 149 | **The worktree**, because several sessions share this machine and some |
| 150 | share this repository, and a checkout has one HEAD, one index and one |
| 151 | working tree between all of them. A branch switch under somebody else's |
| 152 | uncommitted work destroys it with nothing on screen to say so, and two |
| 153 | sessions editing the same paths disagree quietly instead of conflicting |
| 154 | loudly. A worktree gives the work its own HEAD and its own files, and |
| 155 | costs a directory. |
| 156 | |
| 157 | **From `origin/main`, after a fetch** — not from whatever the checkout |
| 158 | happens to be sitting on. A branch cut from a stale main is a merge |
| 159 | conflict scheduled for later, against files somebody else has renamed in |
| 160 | the meantime. A branch cut from another task's branch is worse: it |
| 161 | carries that task's commits into yours, where they are reviewed as |
| 162 | though you wrote them, and the two can only be separated by hand. |
| 163 | |
| 164 | Remove it when the work has landed — `git worktree remove` — so that |
| 165 | `git worktree list` keeps saying something true. |
| 166 | |
| 167 | Written down because of 2026-09-17, in this repository, while it was |
| 168 | still called `meet`. The shared checkout was |
| 169 | sitting on one task's branch while holding a *different* task's feature |
| 170 | uncommitted on disk — some four hundred lines, a new migration among |
| 171 | them — put there by a session nobody else knew was working in that tree. |
| 172 | Two other sessions were meanwhile coordinating over the same checkout by |
| 173 | asking each other not to touch it, which is the arrangement a worktree |
| 174 | makes unnecessary. Nothing was lost. Nothing about the arrangement would |
| 175 | have said so if it had been, and by then the same feature existed twice, |
| 176 | once on each side of a rename, because the two sessions had branched |
| 177 | from different starting points. |
| 178 | |