oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Calls
2
3Calls is video calling for one team. A member makes a meeting, now or
4for later, and sends one link; anyone who follows it sets up their
5camera and microphone on a holding screen, waits to be let in if they
6are not on the team, and talks in a room of up to four, with chat
7beside the call and a screen to share. An agent can also create,
8list, relink and end meetings over an API. Every screen works with
9JavaScript disabled except browser media participation — the camera
10preview and join button on the holding screen, and the call itself in
11the room — and, once a call exists, the room's two other script-only
12parts: the chat panel and the live admission queue with its Admit
13controls. Both pages still render their title, start time, standing,
14floor notice, the paragraph that says what needs script, and a
15member's navigation with scripts off (spec §9).
16
17ICE servers come from the CARLOS platform relay, delivered as
18`CARLOS_RELAY` and minted fresh for each call at `GET /m/{sqid}/ice`;
19Calls names no STUN or TURN server of its own (suite ruling 6). With no
20relay configured, calls are direct only: two browsers connect when ICE
21can find a working path straight between them, which the holding
22screen states plainly above the join button: "This team's Calls has no
23relay yet. Calls usually connect only between people on the same
24network." Enabling or changing the relay restarts the instance. When
25the relay is in the path it forwards DTLS-SRTP packets encrypted
26between the browsers, so it carries bytes it cannot read; Calls' own
27process never receives a media packet at all. With the relay on, a
28call that still fails says "could not connect, even through the
29relay" and offers retry; it does not diagnose the network. One known
30cause is a network that allows only TCP 443, because the relay's TLS
31listener is deferred by the platform — a possibility, not a verdict
32(spec §0.1, §5.2, §5.5, §5.6, §7.3, §15.13).
33
34## Sign-in
35
36Sign-in is a magic link to an address on the team's allowlist
37(`CALLS_ALLOWED_ADDRESSES`). Following a meeting link gives an outsider
38a guest grant at sign-in — never a way into a room without a name —
39and the allowlist otherwise gates sign-in exactly as it does for a
40member (spec §3, §7.1). An operator can also enable Home sign-in with
41all four of `CALLS_HOME_ISSUER`, `CALLS_HOME_CONSUMER_ID`,
42`CALLS_HOME_KEYS_BASE64` and `CALLS_HOME_DEK_BASE64` — never a partial
43set — which links an existing member after a fresh, independent local
44sign-in and explicit confirmation; it never creates a person or grants
45membership on its own (spec §7.1.1). Because a member's address can be
46rate-limited on purpose and gets a different answer from an unknown
47one, whether a given address is on the team is not a secret the
48sign-in form can keep; that residue is stated rather than claimed away
49(spec §3.2).
50
51## The room
52
53The room is a peer-to-peer mesh of up to four admitted participants,
54negotiated with the W3C perfect-negotiation pattern over an SSE stream
55and a POST signal per peer. A guest waits in a queue until an admitted
56member lets them in; removing a participant and ending a meeting each
57go through their own confirm page — there is no in-room control that
58skips one (spec §3.5). A member can retrieve the current invitation
59again from the holding screen or the room with Share (`POST
60/m/{sqid}/share`). On a meeting whose only link predates the
612026-09-12 sharing update, the first Share mints a new recoverable
62invitation alongside the old, still-valid one rather than opening it;
63every Share after that retrieves the same recoverable one. Replacing a
64link revokes every live one. Chat is fanned out live to admitted peers
65and never stored (spec §6). Screen share adds a second tile per sharer
66(spec §5.4). The room lives in one process's memory, so every deploy
67drops every live call (spec §15.11).
68
69## Agents
70
71A member mints an agent's bearer token from Settings; it is shown once
72and the row after it keeps only its hash. The token works on
73`/api/v1` only — never a cookie there, and never a bearer on an HTML
74route — for five operations: list meetings, create one, replace its
75link, end it, and read one meeting's detail. It cannot join a call
76(spec §8). An agent's reach is re-derived from its operator's current
77standing on every request, so an operator who leaves the team takes
78their agent's reach with them at once, with no separate revocation
79step. Requests are limited per agent to one a second sustained, with a
80burst of 60 (spec §8, §15.17).
81
82## Native apps
83
84The SwiftUI Mac and iPhone apps use this same server and a shared Go
85core (`callscore/`, `protocol/`); there are no embedded webviews. Build
86the Mac app with `make native-mac`. See `native/README.md` and
87`native/ios/README.md` for setup, native sign-in and the native gates.
88
89## Building and gates
90
91`CGO_ENABLED=0 make ci` (vet, gofmt, tests, migration check) is the
92one gate a runner and a push share; `ci` itself does not set the
93variable, so the runner and every push must. Three things ship or run
94under `CGO_ENABLED=1`, each isolated from the server: `make race`
95builds a server test binary, never a release, for any change to
96`internal/calls/hub` or the signalling stream; `make native-test`'s own
97`callscore` race sub-step does the same for the shared Go core; and the
98native Mac and iPhone/iPad bindings under `native/` bridge Go to
99Objective-C with real cgo, isolated in `native/go.mod` and built with
100`make native-mac`. `make browser` drives a real call across two real
101Chromium contexts, 31 scenarios in about nine minutes; it needs the
102Claude Code bash sandbox off, because the sandbox blocks UDP and every
103call then fails with `frames` stuck at 0. `make release VERSION=<sha>`
104stamps that version into `/api/version`, so a deploy can be checked
105against what it is meant to be running.
106
107## Deploying
108
109Calls deploys as a CARLOS unit route. Build the artifact first — `make
110release VERSION=<sha>` produces `releases/calls-linux-arm64`, stamped
111with the version `/api/version` will report (`strings -a
112releases/calls-linux-arm64 | grep -cx <sha>` confirms the stamp) — then
113deploy it: `carlos deploy --app calls --account oficina --channel edge
114--version <sha> --target linux-arm64 releases/calls-linux-arm64`,
115which waits until `/api/version` reports it — but only once a build
116probe is configured for the app: `carlos apps build-probe --app calls
117--account oficina /api/version`, set on the flagship app since
1182026-09-09. A new app or account needs that command run once before
119the wait means anything; without it `carlos deploy` watches
120`X-Carlos-Version` instead, which can report the new version adopted
121before the running process has actually changed. `--account` is a
122global flag; `carlos ship` uploads only, while `carlos deploy` is ship,
123promote and wait (its own help text). Building for your own
124architecture and shipping that uploads fine and fails to exec on the
125instance.
126
127These are the settings a deploy needs (`config.go`, `standing.go`).
128Five refuse startup when empty outside `-env development`:
129`CALLS_ORIGIN` (the external origin, scheme included);
130`CALLS_INSTANCE_KEY`; `CALLS_DEK` (base64 of exactly 32 decoded bytes,
131not 32 base64 characters); `CALLS_ALLOWED_ADDRESSES` (comma-separated,
132non-empty — this is the team); and mail, `CALLS_SMTP_*` or the
133platform's `CARLOS_SMTP_*`. Two are optional, each with its own
134default rather than a refusal: `CALLS_ZONE` (an IANA zone, defaulting
135to UTC when unset) and `CARLOS_RELAY` (empty means no relay, §5.5 —
136only a *malformed* value refuses startup), delivered by `carlos relay
137enable --app calls` rather than set by hand. Secrets —
138`CALLS_INSTANCE_KEY`, `CALLS_DEK`, `CALLS_SMTP_PASS` and
139`CALLS_APNS_KEY` — go through `carlos secrets`; the rest go through
140`carlos env`.
141
142Apple push is a sixth group, and all of it or none of it:
143`CALLS_APNS_KEY` (the `.p8` as Apple issues it, PEM and multi-line),
144`CALLS_APNS_KEY_ID`, `CALLS_APNS_TEAM_ID`, `CALLS_APNS_TOPIC_IOS` and
145`CALLS_APNS_TOPIC_MACOS`, with `CALLS_APNS_SANDBOX` for a development
146build's tokens. Unset, the server sends no push and says so nowhere —
147a knock still reaches every member in the room over their own stream,
148which is where this app was before push existed. Partly set, startup
149refuses: half a push key is a mistake to catch at boot rather than in a
150goroutine on the first knock. Mail reads
151`CALLS_SMTP_*` with `CARLOS_SMTP_*` as a per-variable fallback; the
152platform supplies a sender too
153(`CARLOS_SMTP_FROM`), which `CALLS_SMTP_FROM` can optionally override,
154so `carlos email enable` alone is enough to send mail. `carlos relay
155enable --app calls` turns on the platform relay. Every deploy, and
156every relay change, restarts the instance and drops every live call —
157no drain exists, so deploy outside meeting hours. Migrations apply
158once each at boot (`msg=migrate` in the log) and are never re-run; a
159database that already applied a migration file under an earlier name
160fails at boot rather than silently diverging. The deploy wait can time
161out before the hand-over while the instance then serves the new
162version a minute later — check `/api/version` before retrying.
163
164Canary a build on its own host: `carlos canary --app calls --name
165slice2` composed `slice2.calls.bds.oncarlos.com`, and the console
166refused it ("invalid host, an instance host is one DNS label followed
167by .bds.oncarlos.com") — as recorded on 2026-09-10 in platform
168discussion #124, open as of that record. The recipe actually used:
169`carlos instances create --app calls --account oficina --host
170slice2-calls.bds.oncarlos.com --channel canary/slice2 --environment
171slice2`. The `slice2` environment bundle must already exist, created
172separately, carrying `CALLS_ORIGIN` for the canary host; without it
173sign-in refuses on the canary while GETs look healthy. Then `carlos
174deploy --account oficina --app calls --channel canary/slice2 --host
175slice2-calls.bds.oncarlos.com --version <sha>
176releases/calls-linux-arm64`.
177
178## Where things are written down
179
180The design lives in `docs/superpowers/specs/2026-09-07-meet-design.md`,
181the slice plans in `docs/superpowers/plans/`, and each slice's review
182in `docs/superpowers/reviews/`. `PRODUCT.md` covers what Calls is for
183and what it deliberately leaves out; `AGENTS.md` covers the
184conventions the code is held to. Spec §15.18 is the disposition of
185record for what slice 6 delivered, deferred (the acknowledgements
186page) and declined (the Estilo theme pin, the production promotion).
187
188### Meeting access settings
189
190Team members can set **Meeting defaults** in `/settings` and change either
191option for an individual meeting from its **Settings** link. New meetings
192permit guests and have no waiting room unless the team changes those defaults.
193Defaults apply to every creation path, including agents. Existing meetings keep
194their original sign-in and waiting-room requirements during migration.
195
196When sign-in is off, an invitation holder can enter a display name to join as
197a guest. Guest sessions cannot create meetings, change settings, or admit other
198guests. With the waiting room on, a team member admits guests; with it off,
199guests enter directly, subject to the same call capacity limit. Changes apply
200at entry; admitted participants keep their participation. Turning the waiting room off
201admits queued guests up to the call capacity.
202
203Invitation and browser holding pages offer **Join in the app**, using the
204`oficina-calls://join?url=…` URL scheme registered by the Mac and iOS bundles.
205On the invitation page (nobody signed in yet) the link carries the invitation,
206and the app runs its own browser sign-in for it, guests included. On the
207holding page (signed in, as a member or a guest) it goes through
208`/m/{sqid}/app` first, which makes a five-minute, one-use *offer* bound to this
209browser session and opens the app with it (`&offer=…`); the app's ordinary
210native sign-in request names the offer, and the page the person is on turns
211into the confirmation for it, with the app's code — so the app gets this
212person's own standing in this meeting without a second sign-in, in whatever
213browser they happen to be using. An app already signed in as someone who can
214join the meeting uses its own session and needs no browser. Browser joining
215remains available when the app is not installed.
216