> ## Documentation Index
> Fetch the complete documentation index at: https://capy.sc/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Capy Architecture: How CLI and Service Fit Together

> How Capy fits together: CLI handles encrypt, sync, invite on dev machines; service brokers co-decrypt and stores ciphertext only. On-disk layout explained.

Capy has two moving parts: the CLI on your machine, and the service that brokers the co-decrypt handshake. The CLI handles everything local - syncing, encrypting, invites, deploy setup - and spawns your app with decrypted env vars via `capy run`. The service holds only ciphertext and membership records.

## The two components

<Frame>
  <img src="https://mintcdn.com/capy-c95c79c0/N7qpVCkQCaY3Lo-w/images/diagrams/09-arch.svg?fit=max&auto=format&n=N7qpVCkQCaY3Lo-w&q=85&s=9c86a530b5f3bb03528e70368a1c9332" className="block dark:hidden" alt="Two-component architecture: the Capy CLI runs on developer machines and CI, syncs .env, invites, kicks, and wraps your app with capy run. The Capy service brokers the co-decrypt handshake and stores opaque ciphertext plus membership records. The service never sees plaintext." width="437" height="208" data-path="images/diagrams/09-arch.svg" />

  <img src="https://mintcdn.com/capy-c95c79c0/N7qpVCkQCaY3Lo-w/images/diagrams/09-arch-dark.svg?fit=max&auto=format&n=N7qpVCkQCaY3Lo-w&q=85&s=afcffe616469c32594176080945f4598" className="hidden dark:block" alt="Two-component architecture: the Capy CLI runs on developer machines and CI, syncs .env, invites, kicks, and wraps your app with capy run. The Capy service brokers the co-decrypt handshake and stores opaque ciphertext plus membership records. The service never sees plaintext." width="437" height="208" data-path="images/diagrams/09-arch-dark.svg" />
</Frame>

The CLI is the only place plaintext values ever exist. The service only ever sees opaque ciphertext plus membership records.

## On-disk state

Inside a Capy-managed project:

Gitignored: `.env`, `.env.pre-capy.old`, `.capy/`. Committed: `keep.lock`.

```
my-project/
  .env              ← capy snippets
  .env.pre-capy.old ← plaintext backup
  keep.lock         ← versioning manifest
  .capy/
    config          ← last deploy platform
    decrypt         ← hex project key (CI)
```

And globally, in your home directory:

```
~/.capy/
  auth/
    session.json   ← refresh token
    sessions/      ← per-user cache
  orgs/
    {orgId}/
      users/
        {userId}/
          key.enc  ← wrapped master key
      projects/
        {projectId}/ ← cached
```

`key.enc` is the only long-lived secret on disk, and it lives only on the client. It's AES-encrypted with an inner key derived from `SHA256(userId || ":" || orgId)` (never stored anywhere) and then outer-wrapped via the service's `/orgs/{orgId}/wrap` endpoint. The service never stores a copy - to use `key.enc` the CLI presents it for co-decrypt, where the service strips only the outer layer.

## keep.lock

`keep.lock` is a small JSON file that tells Capy which project this directory belongs to and what its current state is. It contains:

* **Org ID and project ID** - which org and project this directory maps to.
* **Schema version** - for format evolution.
* **Variable manifest** - a sorted list of variable names with per-branch resource IDs and value hashes. Hashes, not plaintext, not ciphertext, not keys.

It **doesn't** contain any keys, any plaintext, or anything sensitive. Committing `keep.lock` is what lets a teammate clone your repo, run `capy`, and sync the same secrets you're working with. Active branch is tracked separately in `.capy/branch` (local state).

## .env after Capy

Your `.env` after `capy` has run looks like:

```bash theme={null}
DATABASE_URL=capy:aB3kF:iv+ct+tag-b64
STRIPE_KEY=capy:mP9zQ:iv+ct+tag-b64
REDIS_URL=capy:x7nYt:iv+ct+tag-b64
```

Each `capy:…` snippet is:

* `capy:` literal prefix
* `{resourceId}` - a 5-character hash stable for this `{projectKey, varName}` pair, used to diff without leaking plaintext
* `{blob}` - base64 of `iv || ciphertext || tag` from AES-256-GCM under the project key

The snippets are inert without the project key. Even if someone sees them, they can't tell if two projects share a value, because `resourceId`s are namespaced per project.

## Git hooks

On first-run Capy installs two hooks:

* **`post-checkout`** - runs `capy status` after you switch git branches, so you notice drift immediately.
* **`post-merge`** - same, after `git pull` / `git merge`.

No `pre-push` hook. If an older Capy version installed one, `capy` cleans it out on the next run.

## Sync engine

The sync engine is a three-way merge between:

* **Local** - what's currently in `.env` after any edits you've made.
* **Pinned** - what was in `.env` the last time Capy saw it in sync with the remote.
* **Remote** - what's currently in the service's blob for this branch.

For each variable, Capy picks automatically when only one side changed. When both sides changed (conflict), it prompts interactively. See [Syncing secrets](/using/syncing-secrets).

## The wire

Every CLI request to the service is a plain HTTPS POST with a JSON body and an auth token. The bodies are small and the payloads are already-encrypted blobs - compromising the transport tells an attacker nothing they couldn't get by compromising the service itself.

## What's next

<Columns cols={2}>
  <Card title="Zero trust" icon="shield-halved" href="/internals/zero-trust" horizontal>
    Why two shares, and what each share holds.
  </Card>

  <Card title="Cryptography" icon="lock" href="/internals/cryptography" horizontal>
    Every client-side algorithm, key, and parameter.
  </Card>
</Columns>
