Glossary¶
This product has its own vocabulary and the code uses it everywhere, in class names, table names and filenames. Reading the codebase without it is harder than it needs to be, and two of these terms are easy to confuse in a way that matters.
The product¶
Circle — a closed group of friends. Everything belongs to one: the roulette,
the turns, the vlogs. You join by invite code and there is no discovery, no
search and no public anything. circles table, Circle model.
Cadence — how often the roulette spins in a circle: every 3, 5 or 7 days. Chosen when the circle is created and stored on it, not on the turn.
Draw — the roulette picking whose turn it is. Weighted, not uniform: it
favours whoever has had the fewest turns, and it will not pick the previous
vlogger again unless there is nobody else. app/services/draw.py.
Turn — one person's window to record and publish. It has a deadline, a status, and at most one vlog. The whole product is a state machine over this one object; see Turn states below.
Vlogger — whoever the draw landed on. Only they can record or upload for that turn; the API checks it on every route that touches footage.
Premiere — the window after a vlog is published, during which the circle
can watch it: the circle's premiere_hours, 24 unless the owner changed it
(1 to 168). When it ends the turn is archived and the roulette spins again.
Deadline reminder — one push to the vlogger when fewer than 24 hours
(reminder_hours_before) are left on an open turn. Sent once per turn and
recorded in turns.reminded_at; remind_expiring in app/services/turns.py,
run at the start of every tick.
Strike — what a vlogger gets for letting a deadline pass without
publishing, whether or not they had recorded anything: the footage is on
their phone, and the server only knows nothing was published. A re-roll is
deliberately not a strike: that is the group choosing, not a failure. Nor
is leaving the circle mid-turn -- the turn is missed, but without one.
Strikes are counted and shown, and that is all they do today. They are explicitly not grounds for removing anybody: being thrown out of a group of friends automatically is not something the software should decide, and a circle that ejects people loses the joke. What they are eventually for is in roadmap.md — they are meant to weigh on a second roulette, not on your membership.
Re-roll — the circle voting to hand the turn to somebody else. A strict
majority is enough while the turn is pending or recording; once it is
rendering there is no vote. During a premiere the same vote ends it early,
and that one requires everybody, because it takes the vlog away from
anyone who has not watched yet.
Heartbeat — POST /turns/{id}/heartbeat, the vlogger's phone telling the
server it is recording or rendering, with how many takes it holds and how many
seconds they add up to. The only sign of life a turn gives the circle before
the vlog exists: the footage itself stays on the phone, unless the edit goes
to the server render.
Mute — a member silencing one circle's pushes on their own phones.
memberships.muted, per person and per circle, invisible to everyone else;
the muted member is still drawn, records and votes as before.
Comment — a short line (200 characters at most) a member leaves on a vlog,
next to the reactions, before or after the premiere closes. The vlogger gets
a push about it unless it is their own. vlog_comments table.
The footage¶
These three are not synonyms, and the difference is why some code looks the way it does.
Take — one continuous act of recording: from pressing record to pressing stop. This is the unit a person thinks in, and the unit the review screen shows.
Segment — one file on disk. A take is usually one segment, but flipping
the camera mid-take closes one file and opens another, so a single take can be
several segments. The person did not stop recording, so the edit must not treat
it as two takes. groupIntoTakes in src/storage/clipList.ts is what puts them
back together.
Clip — the generic entry in the manifest: a segment of camera footage, or a video or photo brought in from the gallery. Photos are held as stills for a few seconds and given silence for audio.
Manifest — the JSON file on the phone that lists a turn's clips, their
order, durations and render state. It is the source of truth while recording,
survives the app being killed, and is what lets somebody record on Monday and
finish on Wednesday. src/storage/clipVault.ts.
Allowance — how much raw footage the camera will accept, which is more than the finished vlog can hold (1.05x by default). Recording more than the result can fit is expected; the edit trims the last take to fit and says what did not make it.
The edit¶
EDL — Edit Decision List. A plain description of the finished vlog: which
clip, from which second to which second, with what transition. It is computed
from the manifest and it is deliberately duplicated in TypeScript
(src/edit/edl.ts) and Python (app/services/edl.py), with mirrored tests, so
the phone and the server produce the same vlog from the same footage.
Engine — whatever actually turns an EDL into an mp4. src/edit/engine.ts
picks between three: the native module on the device, an in-process
FFmpeg that no build currently links, and the server fallback. When none
of them can run it says none rather than failing somewhere less obvious.
Native engine — mobile/modules/vlog-editor, a local Expo module. On
Android it decodes, composites through OpenGL and re-encodes with MediaCodec.
This is the architecture the product is built on: the phone edits, the server
stays small.
Server render — the fallback, in app/services/render.py, which does the
same job with FFmpeg. It exists for devices that cannot run the native engine —
Expo Go, a build older than the module, a device without the encoders it
needs — and for material the engine says it cannot carry. It is also the safety
net when the native engine says it can and then fails mid-render:
src/edit/nativeFallback.ts retries the same manifest on the server, if the
server has it switched on. It is off by default because it turns a light
server into a video server.
Loudness normalisation — making takes recorded at different distances from the microphone sit at the same level, so the finished vlog does not jump in volume at every cut.
VAD — voice activity detection, used to find silences. It is implemented and
switched off: cutting a person's pauses reads as the app editing over them
rather than for them. TRIM_SILENCES in src/edit/trimming.ts is the single
constant that decides it.
Turn states¶
Set in app/models/enums.py. The status is visible to the whole circle in
every state, along with the take count and seconds of footage the vlogger's
phone last reported; what stays on the vlogger's phone is the footage.
| State | Means |
|---|---|
pending |
Drawn, nothing recorded yet |
recording |
At least one clip exists on the device |
rendering |
The vlog is being built or uploaded; no re-roll vote now |
published |
Uploaded and premiering to the circle |
missed |
The deadline passed without a published vlog: strike, and the turn moves on. Also where a turn ends, with no strike, when its vlogger leaves the circle |
skipped |
The circle voted to re-roll: no strike, the turn moves on |
archived |
The premiere ended; the vlog is history |
Infrastructure¶
Presigned URL — a signed, expiring permit the server hands the phone so it can upload straight to R2 without the video ever passing through the API. The reason the API machine can stay small.
Deferred push — a push queued while a change is being made and sent only
once that change has committed: _notify queues, and send_after_commit (in
an endpoint, through FastAPI's BackgroundTasks, after the response) or
_committing (in the tick) sends. A commit that fails sends nothing, so no
push ever describes something that was rolled back (#141).
Push token — an Expo push token identifying one app install on one
device, not an account. push_tokens table, one row per device; an account
signed into two devices holds two live tokens and a push reaches both. See
architecture.md.
Development build vs preview build — the development build needs the Metro bundler running on a computer and talks to a local API; the preview build is standalone and points at production. They share a package name, so installing one replaces the other.
Source maps — the translation table that turns a crash in compiled Hermes bytecode back into a filename and a line number. Uploaded to Sentry during the EAS build; without them a mobile stack trace is unreadable.