Privacy
Omelet Clipboard stores as little as your phone book does.
Every claim below names a number and the invariant that bounds it. The status reads enforced where a machine check already runs in CI, and planned where the rule is agreed but the check lands in a later step of the bootstrap plan. The invariant list lives in principles/invariants.md and the architecture in CLUSTER.md.
State as of 2026-08-11: the product is under active development and no service URL is live yet. The checks marked enforced are real and running in CI; the checks marked planned land in the steps named below.
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:
- Capacity per room: 10 MB. A larger payload must go peer to peer or fail with an explicit message rather than a silent retry.
- Lifetime: at most 60 seconds. The buffer deletes on acknowledgement or at a 60 second TTL, whichever is first.
- Location: process RAM only. The buffer writes nothing to disk and nothing to Postgres.
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
- Clipboard payload, encrypted or otherwise.
- Plaintext clip content. Plaintext never leaves your device.
- File contents.
- Transfer history. Nothing is retained after delivery, which rules out a history feature by construction rather than by preference.
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.