What we store

The server database is Postgres with exactly two tables. Their columns, in full:

users(id, google_sub, email, created_at)
devices(id, user_id, name, platform, pubkey, last_seen)

No table holds clipboard content. Not encrypted, not truncated, not for debugging. Adding a column outside this set fails the relay's CI on every PR.

Invariant no-payload-columns -enforced. The allowlist check introspects the migrated schema and fails on any table or column outside it. Bootstrap plan step 5.

What we relay

When two devices cannot establish a direct WebRTC data channel, the clip transits a fallback buffer in the relay process. The design constraints on that buffer are:

Invariants relay-buffer-ttl and no-disk-spill- planned. The integration test that asserts the 60 second TTL, the on-ACK delete, and the no-disk boundary lands in bootstrap plan step 9. Until then the numbers above are the design constraint the relay is being built against, not yet a checked one.

What TURN relays

When NAT traversal fails, devices fall back to TURN. TURN relays ciphertext bytes between the two WebRTC peers. The bytes are sealed on the sending device and opened on the receiving one; TURN sees opaque data, and coturn holds no history after the session closes.

TURN credentials are short-lived. A username ofexpiry:random and an HMAC-SHA1 credential with a 600 second TTL are issued per request from the relay. No TURN username or credential is committed or bundled.

Invariant no-static-turn-creds -planned. The credential shape is covered by the relay's test:ice-credentials (step 8 code landed, step still pending a live check). The full invariant - a gitleaks rule plus a grep of the built app bundle for the coturn realm string - and coturn itself land in bootstrap plan step 11. Until step 11, coturn is not deployed.

Anonymous use

Two devices can move a clip with no account, using a pairing code. The code is six characters over an alphabet with 0 O 1 I Lremoved, single-use, and invalidated the moment the second peer joins. A third joiner is rejected. The code expires at 300 seconds. Codes and rooms live in relay process memory.

Invariant code-single-use -planned. The double-join rejection and the 300 second expiry are checked by an integration test that lands in bootstrap plan step 7.

Account use

Signed-in users authenticate with Google. The access token is a JWT valid for 15 minutes; the refresh token rotates on each use. The server stores your account row and one row per device you sign in on, holding the device name, platform, public key, and last-seen timestamp. A signed-in user sees their device list on a new install without entering a pairing code, and can revoke a device from that list.

Session shape and rotation land in bootstrap plan step 6 (test:token-shape and the auth flow); device registration on sign-in lands in step 6 and step 19. No entry ininvariants.md guards the 15 minute value directly; it is set in the relay configuration and asserted by the token-shape test.

What the wire protocol carries

The relay sees signaling messages (room code, SDP, ICE candidates) and, on the fallback path, opaque ciphertext chunks. Every message kind in the schema has a round-trip test on both the relay and the app, including the data-channel-only kinds the server never sees at runtime.

Invariant wire-protocol-both-sides -enforced. Coverage is gated on the schema's oneOf list; adding a kind with no test fails the build. Bootstrap plan step 4.

What we never store

The payload-column boundary is enforcedby no-payload-columns (step 5). The log-line boundary - payload bytes never reaching a log, a trace, or an error report - is invariant no-payload-logging,planned, landing in bootstrap plan step 10.