Capy is zero-trust: the service stores your encrypted secrets but can never decrypt them on its own. Every decryption requires two independent shares - one held by you, one held by the service. This page walks through the complete client-side cryptography, from the root seed phrase down to the exact bytes your app decrypts at runtime. Service-internal constructions (how the outer wrap is stored, how the service gates co-decrypt requests) are intentionally out of scope here. The guarantee you should rely on is that the service holds only ciphertext and membership records, never key material in recoverable form.Documentation Index
Fetch the complete documentation index at: https://docs.capy.sc/llms.txt
Use this file to discover all available pages before exploring further.
Trust model
Neither side can read plaintext alone.- Share 1 - your machine. Holds the inner wrapping key, any derived project keys, and (for owners) the BIP-39 seed phrase. None of these ever leave your machine.
- Share 2 - Capy service. Holds the outer encryption layer and the membership records that gate every decrypt request. The service never sees plaintext and cannot derive any user key on its own.
key.enc file but still cannot recover any master key - the inner layer is AES-encrypted with a value bound to each user’s identity and never stored server-side.
Key hierarchy
One root, everything else derived.| Key | Size | Scope | Origin |
|---|---|---|---|
| BIP-39 seed phrase | 24 words | Organization root | User-generated |
| Master key M | 32 bytes | Organization | PBKDF2-SHA512 |
| Project key PK | 32 bytes | Project | HKDF-SHA256 |
| Inner wrapping key | 32 bytes | Local storage | SHA-256 |
| Invite token T | 32 bytes | One-time invite | Random |
| Deploy token DT | 32 bytes | One deployment | Random |
Each
capy:… snippet also includes a 5-character resourceId - a stable per-project identifier for the variable that lets Capy diff ciphertext without leaking plaintext or revealing that two projects share a value.Inviting a new member
Capy’s invite flow is “double-wrapped”: the master key is encrypted first by a client-side key derived from a one-time token, then again by the service. The token travels out-of-band to the invitee. The service never sees it.Generate the invite token
Your CLI generates Binding the email into the HKDF salt means only the intended recipient can later derive the key that decrypts the blob.
T = randomBytes(32) and derives an inner key bound to the recipient’s email:Outer-wrap via the service
Your CLI sends
innerBlob to POST /orgs/{orgId}/wrap. The service adds its outer wrap and returns an opaque outerBlob. The service never sees the inner key or M.Build the redeem code
Your CLI concatenates
T, the org ID, and outerBlob into a single base64 string. You deliver this code to the invitee out-of-band (Signal, paper, QR). The service never sees the redeem code.Invitee authenticates
The invitee runs
capy redeem <code> and authenticates, receiving an auth token bound to their email.Service strips the outer wrap
The invitee sends
outerBlob to POST /orgs/{orgId}/co-decrypt with their auth. The service verifies org membership - if the user is not an active member, the request fails here, and they cannot proceed. Otherwise, the service returns innerBlob.Invitee strips the inner wrap
The invitee derives the inner key locally using the token
T and the email claim from their auth token. AES-GCM decrypts innerBlob and produces M. If the auth email doesn’t match the salt the inviter used, decryption fails cryptographically - not by policy.Sync: pushing and pulling secrets
When you runcapy, the CLI unlocks your master key, pulls the latest encrypted secrets from the service, diffs them against your local .env, and writes any changes back. Every secret is encrypted with AES-256-GCM under the project key.
capy:{resourceId}:{iv||ciphertext||tag} snippets. Identical variable sets produce identical content-addressed blobs, so uploads are deduplicated.
Your
.env on disk never contains plaintext. Capy rewrites it in place with capy:{...} snippets after every sync. The plaintext only exists in memory, briefly, during the diff.Deploying to production
Deploying an app adds a second zero-trust flow. You can’t just ship your master key to CI - that would collapse the two-share property. Instead,capy deploy mints a deploy token DT that is itself double-wrapped: you keep one half as a CI secret, the service keeps the other half, and at build time your CI runner presents its half to the service to get the second half back.
Mint time (on the developer machine):
DT half.
Mint
Run
capy deploy. Your CLI generates DT, wraps PK with HKDF(DT, salt=projectId, info="capy:deploy"), sends the inner blob to the service for outer-wrapping, and receives a deployId.Store
Your CLI prints the deploy code. You paste it into your CI secret store (GitHub Actions secrets, Vercel env vars, etc.).
Bootstrap at build time
Your build step reads the deploy code, posts the outer portion to
POST /deploy/{deployId}/decrypt, and receives the material it needs to reconstruct PK locally using its own DT-derived key.Revocation
Removing a member is O(1) - a membership deletion on the service side. No key rotation, no re-encryption of secrets.key.enc on disk is outer-wrapped. To use it, they need the service to strip the outer layer, and the service now refuses them. Their bytes are still on disk but cryptographically inert.
If the kicked user had already decrypted some values and kept plaintext copies elsewhere, those copies are outside Capy’s control. Rotate the specific secrets they had access to through your normal secret rotation process - the revocation above only prevents new decryption.
Cryptographic primitives
A single reference for every client-side algorithm used on the data path.| Purpose | Algorithm | Parameters |
|---|---|---|
| Symmetric encryption | AES-256-GCM | IV = 12 random bytes, tag = 16 bytes |
| Key derivation | HKDF-SHA256 | Per-use salt and info - see key hierarchy |
| Seed → master key | PBKDF2-SHA512 | salt="capy-mnemonic", iter=2048, keylen=32 |
| Inner wrap + resource IDs | SHA-256 | Deterministic hashing |