biga

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.

Your folder goes in one side and a browser tab comes out the other. Redis is optional and only matters with more than one server.
  • 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.

The watcher only says a path may have changed. Content hashes decide what actually did, which keeps it right across editors that write in place, rename over, or delete and recreate.

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 at BIGA_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 .gitignore or .bigaignore re-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.

Only changed files travel, and only after the folder has been quiet. A save that doesn't change a file's hash sends nothing.

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.

The watcher is armed before the tree is read, so an edit made during the first upload is queued, not lost.

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.

Local files are the source of truth. The CLI asks what the server has, uploads the difference, and the link never changes.

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:

EventWhen
connectedFirst, when the stream opens.
checkpointA checkpoint was accepted. Carries the changes.
snapshotThe whole tree was replaced.
session_endedThe 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 stored once, by hash, per session. A checkpoint is metadata that points at blobs, so re-saving a file to an earlier state costs nothing.
  • 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. hash and size always describe the raw bytes, so the encoding is invisible to callers.
  • Binary files are recorded, not stored. They get a row with binary set 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

PropertyWhy it holds
A crash is repaired by running the same commandLocal files are the source of truth, and rejoin diffs against the server.
Unchanged files are never re-sentUploads are keyed by SHA-256, and a file whose hash hasn't changed isn't uploaded again.
Only the broadcaster writesEvery write needs the session's owner token, which only the creating CLI holds.
History is completeCheckpoints are append-only, and content is immutable by hash.