oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

calls

A rastrillo app. Read the framework's own SKILL.md (repo root, or in the module cache) before writing app code — it is the whole app story in ~15KB. The conventions below are load-bearing; the framework enforces most of them mechanically.

Layout

  • internal/<app>/ — the app: models.go (plain GORM structs + Models), migrations.go + migrations/ (Schema, BootSchema), app.go (migrate.Apply + chi router), handlers.go, render.go + templates/. This is SKILL.md's five-file shape.
  • manifest/ — the declarative path, optional and equal: TOML or .go Resource manifests, each generating a full screen set under gen/ (see manifest/README.md for the mounting recipe). A hand-written action file at a generated path (carrying //go:build rastrillo_actions) takes that one screen over, by existence.
  • gen/ — generated when manifests are used; never edit. Regenerate with rastrillo generate.
  • internal/calls/seal/ — the app's only encryption surface. A title is sealed by the store and opened by the store; no handler holds a plaintext title it did not get from store.title. pending_joins is the fingerprinted table: an outsider's address is stored there only as a keyed fingerprint, while persons.email holds a verified address in plaintext, the suite's current shape for a person (spec §2.1, as in Docs, Slides and Calendar). Meeting invitation tokens are looked up by hash and sealed for repeat sharing by members. Legacy hash-only invitations remain valid; first sharing creates a sealed invitation alongside them. Replacing a link revokes all current ones.
  • internal/calls/store.go — every database access. standing.go is the entry authority (spec §2.3), computed per request, never stored.
  • Sign-in: auth.go is Docs' shape with the pending-join exception of spec §3.2 and Slides' same-answer wrapper. Keymail or an emailed link is chosen by the address's domain alone, never by membership (Paul, 2026-10-06). A Keymail address, member or not, gets the continuation page to Keymail (never a 303 out of the form POST: Safari applies form-action to it); every other answer from POST /signin is 303 /signin?sent=1, and the link is mailed after the response by signin_mail.go's worker, only if this request may have one, so neither the answer nor its timing says who is a member. A refusal after verification is the door, rendered in the refusal's own 403 by auth's Refused hook (never a query anyone could type). An emailed link opens the confirm door (RenderConfirm): GET /auth/verify spends nothing, so a mail scanner cannot burn it, and the door's one Sign in button POSTs the token back to /auth/verify, mounted for both methods in both modes. mayRequestLink does the same database work for a member and an outsider and logs nothing on the response path.

Rules the family holds hard

  • 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.
  • Migrations apply at boot, once each, and are never re-run or reversed.
  • Schema changes: edit models.go, then run rastrillo migration generate and read the SQL it writes — it may be a full table rebuild, not just an ADD COLUMN. Never edit a migration that has shipped. Renames are hand-written (rastrillo migration new rename_x) because a rename is indistinguishable from a drop plus an add.
  • Money is integer cents. A float never touches a value a person will be held to.
  • Screens work with JavaScript disabled, with two exceptions: browser media participation (the holding screen's camera preview and join button, and the call itself in the room), and, once a call exists, the room's chat panel and its admission queue's Admit controls; both pages still render their title, standing, floor notice and navigation without script. Destructive actions get their own confirm-page URL.
  • The gate is make ci (vet + gofmt + tests) — the same definition CI runs. Run it before every push. CGO_ENABLED=0 throughout: the stack is cgo-free by design.
  • Native Apple bindings under native/ use cgo to bridge Go to Objective-C. Their toolchain is isolated in native/go.mod; server releases remain CGO_ENABLED=0. Build with make native-mac.
  • make race is the test exception to that rule, and the third gate: -race refuses to build without cgo, so it sets CGO_ENABLED=1. It builds a test binary and never a release, so nothing that ships gains a C dependency. Run it for any change to internal/calls/hub or the stream — a hub behind a mutex, a tick goroutine and one goroutine per stream is more concurrency than make ci can see.
  • make browser is the second gate, for any change to a page or to the client: a real call in real Chromium. It needs the Claude Code bash sandbox off — the sandbox blocks UDP, so Chromium gathers no ICE candidates and every call fails with frames stuck at 0, which reads exactly like a client bug and is not one.

Serving

rastrillo.Resolve + Serve speak the CARLOS activation contract (socket/addr/db argv, LISTENFDS, $STATEDIRECTORY, sidecar run) — the golden deployment target, not a requirement: ./app -addr :8080 serves anywhere. GET /healthz and GET /api/version are answered by the framework, outside app middleware.

Deploying

make release VERSION=<sha> builds and stamps the artifact; carlos deploy --app calls --account oficina --channel edge --version <sha> --target linux-arm64 releases/calls-linux-arm64 ships and promotes it, waiting until /api/version serves it — but only once carlos apps build-probe --app calls --account oficina /api/version has been run for the app (set on the flagship app since 2026-09-09); a new app or account needs that first, or the wait watches X-Carlos-Version instead, which can report the version adopted before the process has actually changed. A migration applied under an earlier filename fails at boot; otherwise migrations never re-run. Every deploy, and every relay config change, restarts the instance and drops live calls — no drain exists. The wait can time out before hand-over — check /api/version before retrying. See the README's Deploying section for the canary recipe.

UX conventions (seeded from profile: considered)

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.

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.

  • [x] Icons — lucide, inline — internal/<app>/icons
  • [x] Fields — label, hint, help and error in one envelope, wired with aria-describedby — ui/field, ui/field-select, ui/field-text
  • [x] Destructive actions — their own confirm-page URL, never a modal — ui/confirm-form
  • [x] State — never colour alone — always a text label too — ui/status-pill, ui/badge
  • [ ] Dates — relative, with the absolute date in a title attribute
  • [ ] Errors — server-rendered beside the field, in plain language
  • [x] Empty states — say what the screen is for, not just that it is empty — ui/empty-state

[x] is enforced by a vendored component: use it, do not hand-roll an equivalent. [ ] is guidance to apply by hand. A convention absent from this list is one rastrillo has no opinion about.

Edit any line. This file is the source of truth, not the profile name. The profile only seeded this list at scaffold time; nothing re-reads it, so changing a line here changes what the app does. A convention deleted from this file is not a convention to reinstate.

Work in a worktree, and catch main up first

Take a worktree for the work. Do not work in the shared checkout.

git -C <repo> fetch origin git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main

Both halves matter, for different reasons.

The worktree, because several sessions share this machine and some share this repository, and a checkout has one HEAD, one index and one working tree between all of them. A branch switch under somebody else's uncommitted work destroys it with nothing on screen to say so, and two sessions editing the same paths disagree quietly instead of conflicting loudly. A worktree gives the work its own HEAD and its own files, and costs a directory.

From origin/main, after a fetch — not from whatever the checkout happens to be sitting on. A branch cut from a stale main is a merge conflict scheduled for later, against files somebody else has renamed in the meantime. A branch cut from another task's branch is worse: it carries that task's commits into yours, where they are reviewed as though you wrote them, and the two can only be separated by hand.

Remove it when the work has landed — git worktree remove — so that git worktree list keeps saying something true.

Written down because of 2026-09-17, in this repository, while it was still called meet. The shared checkout was sitting on one task's branch while holding a different task's feature uncommitted on disk — some four hundred lines, a new migration among them — put there by a session nobody else knew was working in that tree. Two other sessions were meanwhile coordinating over the same checkout by asking each other not to touch it, which is the arrangement a worktree makes unnecessary. Nothing was lost. Nothing about the arrangement would have said so if it had been, and by then the same feature existed twice, once on each side of a rename, because the two sessions had branched from different starting points.