Architecture
Orange Copy Paste is a desktop app plus an optional sync backend. The app is the source of truth for your data; the backend is a relay that never sees plaintext.
The two halves
Section titled “The two halves”The client captures the clipboard, keeps history and notes in local files, holds all key material, and performs every encryption and decryption. The Rust side owns state, persistence, OS integration, and cryptography; React is the UI. With sync off or the server unreachable, the app is fully functional and queues work locally.
The backend verifies the JWTs Supabase issues (it never signs one), stores ciphertext, fans changes out to a user’s other devices and space members over a WebSocket, and hands out presigned URLs for encrypted blobs. It is stateless, so it scales to N replicas; cross-replica delivery goes through Redis pub/sub.
<<screens only>>
React UI
<<state, keys, sync>>
Rust core
<<issues the JWT>>
Supabase Auth
<<ciphertext rows>>
Postgres
<<verifies the JWT>>
API replicas
<<S3 or R2, encrypted>>
Blob storage
<<fans out>>
Redis
The sync flow
Section titled “The sync flow”<<auth key to Supabase>>
1. Sign in
<<salt, wrapped UMK>>
2. Bootstrap
<<KEK, in memory>>
3. Unwrap UMK
<<WebSocket>>
6. Live changes
<<last write wins>>
5. Push and pull
<<key, device copy>>
4. Register device
- The client stretches the account password with Argon2id, salted from the email, and splits the result in two. It signs in with Supabase directly using one half, the auth key. The password itself is never sent.
- It bootstraps the account and receives the account’s key salt and the password-wrapped master key.
- It derives the key-encryption key from the other half and that salt, and unwraps the User Master Key in memory. A wrong password is simply a failed decryption.
- It registers the device, publishes the device’s public key, and stores a copy of the master key wrapped for that device, so the device can open it again later without the password.
- Entries are encrypted locally, pushed, and pulled. Merges are last-write-wins on
updated_at. - The WebSocket streams live changes from other devices and space members.
<<pushes a change>>
Device A
<<stores it>>
Replica 1
<<pub/sub>>
Redis
<<decrypts it>>
Device B
<<holds the socket>>
Replica 2
The keys and what each one protects are explained in the technical security model.
Deletes travel as tombstones (a push carrying a deletion marker, not a delete request), and a tombstone wins a conflict. Sharing is one primitive, the Space: persistent, multi-member, with a random space key distributed to members by wrapping it for each member’s X25519 public key.
Where the contract lives
Section titled “Where the contract lives”Each specification lives in the repo next to the code that enforces it, so the code stays the one place it is edited. The Reference pages below mirror those files here, read-only, and each one links back to its home in the source repo. Read them before changing anything that crosses the wire.
| Specification | Reference page |
|---|---|
| Wire contract: routes, payloads, DDL, socket events, crypto envelope | Backend architecture |
| Client internals: state, commands, events, persistence, sync engine | Client architecture |
| Cross-component map and invariants | Architecture map |
| Who may do what, and where it is enforced | Permissions |
| How a release gets cut | Releasing |
| Notable regressions and their causes | Bug fix history |