Skip to content

One trip through the whole system

The fastest way to understand this codebase is to follow a single action all the way down. This is what happens between somebody tapping Publicar and their friends being able to watch, touching seven layers on the way. Terms in bold are in the glossary.

Everything before this point — the draw, the recording, the edit — is covered where it happens below.

0. Before the button exists

By the time anyone can publish, three things have already happened.

The draw. A scheduler tick found a circle whose turn had ended, or one that had never had a turn at all, and picked a vlogger (app/services/turns.py::tickapp/services/draw.py). The weighting favours whoever has had fewest turns and excludes the previous vlogger unless they are the only candidate. The circle gets a push notification.

The recording. app/record/[turnId].tsx. Each take is written to the phone's sandbox and appended to the manifest (src/storage/clipVault.ts). Nothing is uploaded and nothing touches the server, so somebody can record on Monday and finish on Wednesday. Flipping the camera closes one segment and opens another inside the same take.

The edit. src/edit/ builds an EDL from the manifest, chooses an engine (engine.ts), and renders. On Android that is the native module: decode → OpenGL → re-encode, one encoder for the whole timeline. The result is one mp4 in the app's sandbox, and the review screen plays it.

1. The tap

app/review/[turnId].tsxpublishVlog() in src/edit/publish.ts.

The publish button deliberately sits above the editing controls. The premise of the product is that nobody wants to edit, so the default path has to be the shortest one; reordering and deleting are there for when something is wrong, not as a step everyone walks through.

2. Asking permission to upload

POST /turns/{id}/upload-urlapp/api/turns.py.

The server checks the caller is the vlogger and the turn is still open, works out the object key (circles/{circle}/vlogs/{turn}.mp4) and returns a presigned URL — a signed permit that expires. It does not accept the file.

This is the hinge of the whole architecture. If the video went through the API, the machine would need to be big enough for everybody watching a premiere at once, which is the one moment they all arrive together. Instead the server authorises a transfer it never carries. See the diagram in architecture.md.

3. The upload

Back in publish.ts: a plain HTTP PUT from the phone straight to Cloudflare R2, with the headers the server said to send and nothing else.

Retried with exponential backoff, because this is a phone on mobile data. The retry is safe because the object key is derived from the turn, so a second attempt overwrites the first rather than leaving debris.

Almost everything that ever goes wrong with R2 goes wrong in the gap between signing and uploading, because the signature covers exactly the headers the client sends. scripts/r2_check.py exists to cross that gap on purpose.

4. Telling the server it worked

POST /turns/{id}/complete.

The server confirms the object really exists in the bucket — it does not take the client's word for it — creates the Vlog row, moves the turn to published, and sets the premiere to end 24 hours later.

Idempotent, deliberately. A phone that uploads, loses signal before the reply and retries must not create a second vlog or restart the premiere.

5. Everybody else finds out

The push goes to every member of the circle except the vlogger (app/services/push.py). Sending is fire-and-forget: a notification that fails must never stop a vlog from publishing.

6. Watching

app/watch/[turnId].tsx asks the API for the turn, which includes a playback_url. With a CDN domain configured that is a plain public URL and the video is served from Cloudflare's edge — the API machine is not involved at all. Without one it is signed against the bucket instead.

POST /vlogs/{id}/view records that somebody watched, which is what the "who has seen it" count and the unanimous early-end vote are built on.

7. And round again

24 hours later a scheduler tick finds the premiere has expired, archives the turn and draws the next vlogger. The same tick() also hands out strikes for deadlines that passed with nothing recorded.

tick() is idempotent and runs every 15 minutes in production. Both facts matter: idempotent because it must survive being run twice, and 15 minutes because the database sleeps between ticks — see limits.md.

  • gotchas.md — the things that will confuse you, and cost hours if nobody tells you first.
  • architecture.md — why the pieces are arranged this way.
  • decisions.md — why they are not arranged some other way.