Architecture
How a save in your editor becomes a checkpoint in someone else's browser tab.
biga is three programs and a database: a CLI that watches a folder, a server that stores what the CLI sends and relays it, and a viewer that renders it. Nothing is pushed from the server to your machine, and the viewer never talks to the CLI.
The whole system
Your folder is read by the biga CLI on your machine. The CLI uploads over HTTP to the server's REST API, which writes to Postgres and, optionally, publishes events through Redis. The server streams events to viewers' browsers, which read state and file content over the same API.
- The CLI is one Go binary. It is the only thing that ever reads your files.
- The server is one Go binary. It accepts uploads over REST and delivers live events over server-sent events. See the HTTP API. The site and viewer are a separate Next.js app that holds no data.
- Postgres is the source of truth for every session. Redis is optional and only needed to run more than one server instance.
- The viewer is the page a link opens. It loads a session's state once and then follows a stream.
There are no accounts anywhere. The session ID is the credential to watch, which is why the CLI is careful about what it shares. Writing needs more: the owner token the server issued when the session was created, and an access key if the server requires one.
What the CLI does between a save and an upload
Six stages in order: watch for changed paths, filter them through the ignore rules, batch them until the folder is quiet, read and hash the files, diff against the last synced hashes, then upload the difference. If nothing differs, nothing is sent.
Three details are worth knowing:
- A burst is one checkpoint. Every change restarts a quiet timer
(
BIGA_IDLE_MS, 120 ms). A long burst is cut atBIGA_MAX_WAIT_MS(1 s) so viewers never wait on you for long. Flushes run one at a time, in order, so uploads never overlap or reorder. - Hashes decide, not events. A path the watcher reports is re-read and hashed, and only a different hash becomes a change. Saving a file without changing it sends nothing.
- An ignore file is a change too. Editing a
.gitignoreor.bigaignorere-checks every synced file and rescans the tree, so newly ignored files disappear for viewers and newly included ones appear.
One save, end to end
The editor writes a file and the CLI sees a filesystem event. After the folder has been quiet for 120 milliseconds, or one second into a burst, the CLI re-reads the paths, hashes them and diffs. It posts a gzip checkpoint, the server answers 202, assigns a sequence number, writes blobs and moves the head, then sends a checkpoint event to the viewer. The viewer updates its tree and timeline and fetches a file's content by hash only when it is opened.
The upload is answered with 202 as soon as it is accepted. The server assigns
the checkpoint a sequence number that is monotonic per session, writes the
content it hasn't seen before, moves the head of the tree, and then publishes
the event. A viewer applies the event to its tree and timeline, and fetches a
file's content only when someone opens it.
Starting and rejoining
A new session uploads a full snapshot first, then streams changes.
The CLI creates the session with a PUT, arms the file watcher, scans the folder and uploads a full snapshot. The server stores the tree and blobs. When a viewer opens the link it loads the state and then opens the event stream, which answers with a connected event.
Running biga start again in the same folder finds the session ID saved for
it and rejoins. Local files are always the source of truth, so the CLI
asks the server for its manifest, uploads only the difference, and the link
never changes.
The CLI reactivates the session with a PUT, asks the server for its manifest of paths, hashes and sizes, and diffs its local tree against it. It posts a checkpoint with only the changed files, and the server sends a checkpoint event to viewers. If nothing differs, nothing is uploaded.
If the manifest isn't available (an older server), the CLI falls back to a full snapshot. Changes made while the server cannot be reached are buffered and sent when it comes back.
Live delivery
A viewer holds one GET /api/sessions/:id/stream open. The server writes four
kinds of event on it:
| Event | When |
|---|---|
connected | First, when the stream opens. |
checkpoint | A checkpoint was accepted. Carries the changes. |
snapshot | The whole tree was replaced. |
session_ended | The broadcast stopped, or went quiet for too long. |
Viewers only receive, so plain server-sent events are enough: ordinary HTTP, and the browser reconnects on its own. When a stream drops, the viewer re-reads the state and merges what it missed.
A broadcast that stops sending heartbeats is marked ended once its session has been quiet for 45 seconds, so a laptop that died doesn't look live forever. The CLI sends a heartbeat every 10 seconds, and skips one when a checkpoint was just sent, since the server counts that as a sign of life too.
More than one server
Events are delivered to the viewers connected to the instance that received
the checkpoint first, and then published to Redis for the others. Each
instance stamps what it publishes and ignores its own echo, so nothing is
delivered twice. Without REDIS_URL everything stays in the process.
Needs a long-running host
Holding a stream open for every viewer means the server has to be a long-running process. A purely serverless or edge deployment can't fan live events out.
The data model
A session has one snapshot of the current tree, many checkpoints, many head files and many blobs. A checkpoint has many changes, and changes and head files both point at blobs by hash.
- Content is addressed by hash. Each unique
(session, sha-256)is stored once, as raw UTF-8 or brotli, and referenced by hash from the tree, the head files and every checkpoint change.hashandsizealways describe the raw bytes, so the encoding is invisible to callers. - Binary files are recorded, not stored. They get a row with
binaryset and no bytes. - An upload is one transaction. The checkpoint, its changes, new blobs and the moved head commit together with the session row locked, so writers to a session take turns and a viewer never sees half an upload.
- A session cascades. Deleting a session deletes everything under it, which is what the retention sweep relies on.
Eager and lazy loading
When a viewer opens a small session the server sends every current file's content with the state, so files open instantly. A session over roughly 1.5 MB or 400 files is served lazily instead: the state carries the tree, and content is fetched per file, by hash, as it is opened. Both thresholds are configurable.
Properties worth relying on
| Property | Why it holds |
|---|---|
| A crash is repaired by running the same command | Local files are the source of truth, and rejoin diffs against the server. |
| Unchanged files are never re-sent | Uploads are keyed by SHA-256, and a file whose hash hasn't changed isn't uploaded again. |
| Only the broadcaster writes | Every write needs the session's owner token, which only the creating CLI holds. |
| History is complete | Checkpoints are append-only, and content is immutable by hash. |