Collaborate

How collab works

The architecture under the rig workspace verbs — tapd, the relay, bindings, and what happens when you edit a file.

This page is for the curious, the cautious, and the operators. For day-to-day collaboration, you don't need it — the rig workspace verbs are the surface.

If you want to know what's actually happening when you save a file in a shared rig, read on.

The pieces

┌────────────┐                       ┌──────────────┐
│  rig CLI   │  shells out to        │  tapd        │
│  (you)     │ ─────────────────►   │  (daemon)    │
└────────────┘                       │              │
                                     │  watches FS  │
                                     │  pushes/     │
                                     │  pulls       │
                                     └──────┬───────┘
                                            │ HTTPS

                                  ┌────────────────────┐
                                  │  tap relay         │
                                  │  (coordinator)     │
                                  │                    │
                                  │  - bindings        │
                                  │  - devices         │
                                  │  - invites         │
                                  │  - change log      │
                                  └────────────────────┘
                                            ▲ HTTPS

                                     ┌──────┴───────┐
                                     │  tapd        │
                                     │  (teammate)  │
                                     └──────────────┘

Three pieces:

  1. rig CLI — what you type. For sync, it shells out to tapd.
  2. tapd — the local sync daemon. One per workspace per machine. Watches the filesystem, ferries change events to/from the relay.
  3. Tap relay — the coordinator. Hosted at https://tap-relay.fly.dev by default. Stores bindings, devices, invites, and a change-event log.

Identity: who's who

Rig identity lives at Rig Hub. rig login does a loopback OAuth flow against the Hub's /auth/device page, signs you in via Clerk, and writes a session JWT to ~/.config/rig/config.json as hub_token. That JWT is what rig and tapd present to the tap relay for the two account-facing actions: creating a binding (rig share) and accepting an invite (rig join).

After binding/join, the per-machine capability token in .rig/tap-binding.local.json takes over for ongoing sync. The relay re-checks on every sync request that the bound user is still a member of the binding, and intersects their requested op with their current role — so removing or demoting a member takes effect on their next request.

Bindings, members, devices, invites

A binding is the unit of collaboration. It has:

  • An ID (bnd_…).
  • An owner (a member with role=owner).
  • A set of members (binding_members rows, each with a role: owner / editor / viewer).
  • A set of devices — one per (member, machine) pair, plus any pure-capability devices.
  • A set of outstanding invites.
  • A change-event log.

A member is a Rig Hub account that's part of a binding, with a role. Inspect with rig who, change with rig role, remove with rig unshare. Owners manage members and invites; editors read/write/subscribe; viewers read/subscribe only.

A device is a per-machine credential within a binding. Created on rig share (for the owner) or rig join (for invited collaborators). Each device has its own capability token stored in .rig/tap-binding.local.json (mode 0600, never committed, never synced). One member can have multiple devices.

An invite is a relay-issued credential that grants the holder permission to claim a new device on the binding. Invites come in two flavors:

  • Member invites (rig share <email> --role …): the accepter joins binding_members and gets a member-tied capability token. Manageable thereafter via rig who / rig role / rig unshare.
  • Pure-capability invites (rig share --ops …, no role): the accepter gets a device-only token with no member row. To kick them, revoke the invite with rig unshare <inv_…> — the cascade kills their token.

Invites are optionally scoped (--ops, --path), email-bound, expiry-bound (--expires), and have a --max-uses counter.

What gets synced

tapd watches files that match [package].include and aren't in [package].exclude, .rigignore, or [local].dirs.

It does not sync:

  • Anything in the default-excluded list (.env, *.key, node_modules/, etc).
  • .rig/tap-binding.local.json (per-device).
  • .rig/instance.toml (per-install).
  • Files inside [local].dirs.

What happens when you edit a file

  1. You save data/templates/email.md.
  2. tapd (running locally) sees the FS change event.
  3. It computes a delta + content hash.
  4. It POSTs to the relay: POST /v1/bindings/<id>/changes with the event.
  5. The relay appends the event to the binding's change log and broadcasts to other devices.
  6. Other devices' tapd instances pull the event, verify the hash, and write the file locally.

In practice this is sub-second on a good network.

The change-event log

Every write/edit/delete is recorded. This is what powers rig history and rig restore.

Each event has:

  • cursor (monotonic sequence per binding)
  • path
  • actor (device + user)
  • kind (write / edit / delete)
  • hash (content hash for verification)
  • timestamp

The log is append-only. It's the source of truth for "what happened on this binding."

Endpoints used by the workspace verbs:

  • GET /v1/bindings/<id>/changes — list events (path-filtered for history).
  • GET /v1/bindings/<id>/blobs/<hash> — fetch a specific blob for restore.

What the relay sees

The relay sees: paths, content hashes, file bytes (stored as content-addressed blobs in S3/R2-compatible object storage), who made each change, when, and the full change-event log. There is no end-to-end encryption in v1 — operators of the relay can in principle read your files.

If that's not acceptable for your data, run your own relay. The relay is the @tap/relay package; it's deployable via the Dockerfile + fly.toml at the repo root, or any Node 20+ host with Postgres + S3-compatible storage attached.

Revoking access

Three surfaces, depending on intent:

  • Remove a member: rig unshare <email> (owner-only). Deletes the member row and cascade-revokes their capability tokens for this binding in one transaction. Their next sync attempt fails with 401.
  • Demote a member: rig role <email> viewer. Token stays valid; ops are intersected with the new role on each sync request, so writes start failing immediately. No token rotation required on the member's side.
  • Revoke an invite: rig unshare <inv_…>. Stops future joins via that invite and cascade-revokes any tokens already minted through it (useful for pure-capability invites where there's no member row to remove).

A revoked or removed user's local file copy remains intact on disk — sync just stops.

Why this name salad? (rig, tap, tapd)

  • rig is the workspace product. CLI, manifest, artifacts, Hub.
  • tap is the sync system. It exists because rig needed a way to make workspaces collaborative without coupling that into the rig CLI. The relay is part of tap.
  • tapd is the local daemon implementation. You install it (@rigxyz/tapd); you don't drive it directly. The rig workspace verbs shell out.

For users: you interact with rig share / rig who / rig status. The rest is plumbing.

See also