Cloud Sync

lpm env push and lpm env pull round-trip the local vault to lpm.dev's encrypted storage. The server stores ciphertext + a wrapped key — it cannot read either. Decryption happens on machines that hold the wrapping key in their OS keychain.

Cloud sync requires a Pro or Org plan. A free account that calls push or pull gets 403 Vault sync requires a Pro or Org plan.

Basic flow

lpm env push                               # encrypt + upload the local vault
lpm env pull                               # download + decrypt
lpm env diff                               # what would change on push/pull

A successful push increments a server-side version. A successful pull re-encrypts under the current local wrapping key and writes to the keychain. Both commands are idempotent — running push twice with no local changes is a no-op.

Architecture

Three keys, never interchanged:

  1. Wrapping key — a 256-bit random key generated once per machine, stored in the OS keychain as dev.lpm.vault-key. Never leaves the machine.
  2. Per-vault AES key — a 256-bit AES key generated per vault on the client. Encrypts the secret blob with AES-256-GCM.
  3. Wrapped key — the per-vault AES key, encrypted with the wrapping key. Stored on the server alongside the encrypted blob.
[your machine]                           [lpm.dev]
                                          (server stores both,
{ FOO: "bar", DB: "..." }                  decrypts neither)
        │                                       ▲
        ▼ AES-256-GCM (per-vault key)            │
[encryptedBlob]                                  │
                                                 │
[per-vault AES key]                              │
        │                                        │
        ▼ wrapped with [wrapping key]            │
[wrappedKey] ────────────────────────────────────┘

To decrypt on another machine, that machine needs the wrapping key. Two ways it gets there:

  • Pair a browser session (Dashboard Pairing) and the dashboard transports a wrapped copy of the wrapping key to that session — encrypted under an ECDH key the browser generated.
  • For team members in an org, see Org Sharing — each member's wrapped copy of the AES key uses X25519 ECIES, not the wrapping key.

What syncs alongside the encrypted blob

vault_sync row:
├── encryptedBlob          ← AES-256-GCM ciphertext (server can't read)
├── wrappedKey             ← AES key wrapped with the user's wrapping key
├── ciEscrowWrappingKey    ← (optional) wrapping key escrowed for CI/OIDC
├── schema                 ← lpm.json > envSchema (synced on push)
├── name                   ← project name (so the dashboard can label it)
├── version                ← auto-incrementing for conflict detection
└── userId or orgId        ← scope

The schema travels alongside the blob so the dashboard can render required-key indicators without seeing values. It's rebuilt from lpm.json on every push. A malformed lpm.json logs a warning and proceeds without schema so a parse error doesn't block a push.

The name field is resolved from package.json > name, then lpm.json > name, then the project directory's basename — useful so the dashboard renders apps/web instead of a vault-id.

Conflict detection

Every push increments version server-side. If your local copy is v3 and the server has v4 from a teammate's push, the server returns 409 VaultConflictError:

$ lpm env push
✗ Vault conflict — local is v3, server is v4
  Pull, reconcile, and re-push.
$ lpm env pull
$ lpm env diff
$ lpm env set DATABASE_URL=...    # edit
$ lpm env push                    # → v5

No silent last-writer-wins. The CLI surfaces the conflict, you reconcile, you re-push.

Signed responses

Successful push and pull responses carry an X-LPM-Signature header — HMAC-SHA256 over the response body, keyed by SHA-256(auth_token). The CLI verifies the signature before parsing or decryption. A missing header on a 2xx, a non-base64 header, or a body that doesn't match the signature aborts the sync with a named error — never a silent fallback to the unsigned body.

What this protects is the envelope — version, wrappedKey, vaultId, metadata — not the ciphertext (AES-GCM already authenticates that). A TLS-terminating intermediary or a compromised CDN cache cannot forge a successful response or replay an old version under a different one.

Because the HMAC is keyed by SHA-256(auth_token), rotating your auth token (lpm token-rotate) automatically rotates the HMAC key on both ends. There's no separate signing-key rotation story.

Key rotation

lpm env rotate-key                         # generate new AES key, re-wrap, push

This generates a fresh per-vault AES key, decrypts under the old key, re-encrypts under the new one, re-wraps for every member (org vaults), and pushes. Useful after offboarding a teammate (see Org Sharing) or after a suspected compromise.

Audit log

Every server-side mutation produces an audit entry: push, pull, share, unshare, rotate_key, member_added, member_removed. Each entry records the user, optional org, action, metadata, and timestamp.

lpm env log                                # last 50 entries
lpm env log --json

For pagination and time-range queries, use the dashboard. Personal vaults show the owner's entries; org vaults show the full audit to owner / admin / maintainer roles.

Wrapping-key migration

Older clients derived the wrapping key from SHA256("lpm-vault-wrap:" + auth_token), which tied the vault to your token — rotating broke decrypt. Current clients use an independent random wrapping key. On decrypt failure with the stored key, LPM tries the legacy derivation as a fallback. If the legacy key works, the blob is re-encrypted under the stored key and pushed back inside the same pull — migration is automatic.

If you've rotated tokens and the data was last encrypted under the old derived key and you no longer have the old token, the legacy path can't recover. Recovery options: pair a still-paired machine and transfer the wrapping key, push fresh data from a machine with intact local secrets, or pull a platform integration that holds the canonical copy.

When to push, when to pull

You want to …Run
Snapshot your current local edits to lpm.devlpm env push
Adopt a teammate's changes onto your machinelpm env pull
Check what push or pull would changelpm env diff
Validate the remote schema statelpm env validate
See who touched the vaultlpm env log

Next