# Cloud Sync Architecture — kvsecure.com

> Design document for the kv cloud sync server. Plan first, build second.
> Referenced from LEDGER.md.

## What We're Building

A zero-knowledge sync server where the server structurally CAN'T decrypt
your secrets — not "we promise not to" but the code doesn't have the
capability. The server stores encrypted blobs and metadata. Nothing else.

## The API Contract Is Already Defined

The client code (`remote.py`) specifies every endpoint. The server must
implement exactly these — no more, no less:

```
Auth:
  POST /auth/register    → {user_id, email, token, refresh_token}
  POST /auth/login       → {user_id, email, token, refresh_token, team_id}
  POST /auth/refresh     → {token}

Sync:
  POST /sync/push        → {version, pushed_at}
  GET  /sync/pull        → {blob, version, blob_hash, pushed_at}
  GET  /sync/status      → [{env_name, version, blob_hash, pushed_at}]

Teams:
  POST   /team/create    → {team_id, name}
  POST   /team/invite    → {ok}
  GET    /team/members   → [{user_id, email, role, joined_at}]
  DELETE /team/revoke    → {ok}

Tokens:
  POST   /tokens/create  → {token_id, token, name}
  GET    /tokens/list    → [{token_id, name, scopes, ...}]
  DELETE /tokens/revoke  → {ok}

Health:
  GET    /health         → {status, db}
```

## Zero-Knowledge Model

### What the server stores
- Encrypted .enc blobs (base64-encoded ciphertext)
- Blob hashes (SHA-256 of the raw .enc bytes — for conflict detection)
- Version numbers (monotonic counter per project/env pair)
- User accounts (email + bcrypt password hash)
- Team membership (user_id + team_id + role)
- CI/CD tokens (scoped, hashed)

### What the server NEVER sees
- Master keys
- Plaintext secret names or values
- Environment key material
- Passphrase or TOTP secrets

### Why it's structurally zero-knowledge
The server receives .enc blobs that are already encrypted with ChaCha20-Poly1305
using a per-environment key derived from the master key. The master key never
leaves the client. The server has no derivation path to any key. Even if the
server's database is fully compromised, the attacker gets ciphertext blobs
that are computationally infeasible to decrypt.

### Team key sharing
kv uses a shared symmetric master key model. Team members share the same master
key via `kvkey_` tokens (out-of-band: Signal, 1Password, secure channel).
All members can encrypt/decrypt the same .enc blobs.

Trade-off: simpler than Bitwarden's per-member asymmetric model. Key revocation
requires generating a new key and re-sharing — acceptable for small teams.

## Deployment Architecture

### Cloudflare Workers + D1

```
Client (kv CLI)
    │
    │ HTTPS
    ▼
┌─────────────────────────────────┐
│   Cloudflare Worker (edge)       │
│                                   │
│   Routes:                         │
│     /auth/*    → auth handler     │
│     /sync/*    → sync handler     │
│     /team/*    → team handler     │
│     /tokens/*  → token handler    │
│     /health    → health check     │
│                                   │
│   Middleware:                     │
│     - JWT verification            │
│     - Rate limiting               │
│     - CORS                        │
│     - Request logging             │
└───────────┬───────────────────────┘
            │
            ▼
┌─────────────────────────────────┐
│   Cloudflare D1 (SQLite edge)    │
│                                   │
│   Tables:                         │
│     users       — accounts        │
│     teams       — team metadata   │
│     members     — team membership │
│     blobs       — encrypted .enc  │
│     tokens      — CI/CD API keys  │
│     audit_log   — push/pull log   │
└─────────────────────────────────┘
```

### Why Cloudflare Workers, not a traditional server

| Factor | Workers + D1 | Traditional VPS |
|--------|-------------|-----------------|
| Ops burden | Zero — no server to maintain | Patching, monitoring, backups |
| Cost (early) | Free tier: 100K req/day, 5GB D1 | $5-20/mo even with zero users |
| Global latency | Edge deployment, <50ms worldwide | Single region, latency for remote users |
| Scaling | Automatic | Manual, requires planning |
| Auth | Built-in patterns, JWT at edge | Self-managed, library dependent |
| Database | D1 (SQLite) — perfect for this scale | PostgreSQL — overkill until 10K+ users |
| Blob storage | D1 BLOB columns (.enc files are <10KB) | S3/R2 needed separately |
| SSL/domain | Automatic via Cloudflare | Let's Encrypt setup needed |
| DDoS protection | Built-in | Need Cloudflare/nginx in front anyway |

### Why D1 (SQLite), not PostgreSQL

.enc files are typically 200 bytes to 10KB. A D1 database can store 5GB on the
free tier. At 10KB average per blob, that's 500,000 environment blobs before
hitting limits. For a product with <1000 users, D1 is more than sufficient.

If/when kv outgrows D1, migrating to PostgreSQL is straightforward — the SQL
is standard SQLite, compatible with Postgres with minor syntax changes.

## Database Schema

```sql
-- Users
CREATE TABLE users (
    id          TEXT PRIMARY KEY,      -- UUID
    email       TEXT UNIQUE NOT NULL,
    password    TEXT NOT NULL,          -- bcrypt hash
    created_at  TEXT NOT NULL,          -- ISO 8601
    team_id     TEXT REFERENCES teams(id)
);

-- Teams
CREATE TABLE teams (
    id          TEXT PRIMARY KEY,      -- UUID
    name        TEXT NOT NULL,
    owner_id    TEXT NOT NULL REFERENCES users(id),
    created_at  TEXT NOT NULL
);

-- Team membership
CREATE TABLE members (
    team_id     TEXT NOT NULL REFERENCES teams(id),
    user_id     TEXT NOT NULL REFERENCES users(id),
    role        TEXT NOT NULL DEFAULT 'member',  -- 'owner' or 'member'
    joined_at   TEXT NOT NULL,
    PRIMARY KEY (team_id, user_id)
);

-- Encrypted blobs (the actual secrets — server can't decrypt these)
CREATE TABLE blobs (
    team_id     TEXT NOT NULL,
    project_id  TEXT NOT NULL,
    env_name    TEXT NOT NULL,
    blob        TEXT NOT NULL,          -- base64-encoded .enc file
    blob_hash   TEXT NOT NULL,          -- SHA-256 of raw bytes
    version     INTEGER NOT NULL DEFAULT 1,
    pushed_by   TEXT NOT NULL REFERENCES users(id),
    pushed_at   TEXT NOT NULL,
    PRIMARY KEY (team_id, project_id, env_name)
);

-- CI/CD API tokens
CREATE TABLE tokens (
    id          TEXT PRIMARY KEY,      -- UUID
    team_id     TEXT NOT NULL REFERENCES teams(id),
    name        TEXT NOT NULL,
    token_hash  TEXT NOT NULL,          -- bcrypt hash of the raw token
    token_prefix TEXT NOT NULL,         -- first 8 chars for identification
    scopes      TEXT NOT NULL DEFAULT '["pull"]',  -- JSON array
    project_id  TEXT,                   -- scope to specific project (optional)
    env_names   TEXT,                   -- scope to specific envs (JSON array, optional)
    created_by  TEXT NOT NULL REFERENCES users(id),
    created_at  TEXT NOT NULL,
    last_used   TEXT,
    expires_at  TEXT                    -- optional expiry
);

-- Server-side audit log (who pushed/pulled what, when — no content)
CREATE TABLE audit_log (
    id          TEXT PRIMARY KEY,
    team_id     TEXT NOT NULL,
    user_id     TEXT NOT NULL,
    action      TEXT NOT NULL,          -- 'push', 'pull', 'invite', 'revoke', etc.
    project_id  TEXT,
    env_name    TEXT,
    ip_address  TEXT,
    created_at  TEXT NOT NULL
);

-- Indexes
CREATE INDEX idx_blobs_team_project ON blobs(team_id, project_id);
CREATE INDEX idx_tokens_team ON tokens(team_id);
CREATE INDEX idx_audit_team ON audit_log(team_id, created_at);
CREATE INDEX idx_members_user ON members(user_id);
```

## Auth Design

### User authentication flow
```
Register:
  Client: POST /auth/register {email, password}
  Server: hash password (bcrypt, cost=12), generate UUID, create user
  Server: generate JWT (1h) + refresh token (30d)
  Client: stores in ~/.kv/session.json

Login:
  Client: POST /auth/login {email, password}
  Server: verify bcrypt hash, generate JWT + refresh
  Client: stores session

Token refresh:
  Client: POST /auth/refresh {refresh_token}
  Server: validate refresh token, issue new JWT
  Client: updates session
```

### JWT structure
```json
{
  "sub": "user_id",
  "email": "user@example.com",
  "team_id": "team_uuid",
  "iat": 1711500000,
  "exp": 1711503600
}
```

Signed with HMAC-SHA256 using a server secret. 1-hour expiry.
Refresh tokens: random 64-byte hex string, stored hashed in DB, 30-day expiry.

### CI/CD token authentication
```
Token format: kvt_<32 random hex chars>
Storage: bcrypt hash in DB, prefix (first 8 chars) for lookup
Header: Authorization: Token kvt_abc123...

Scopes:
  - "pull"  — can pull blobs only
  - "push"  — can push and pull
  - "admin" — full access (team management)

Restrictions:
  - project_id — limit to specific project
  - env_names  — limit to specific environments (["staging", "prod"])
  - expires_at — optional TTL
```

## Sync Protocol

### Push flow
```
Client:
  1. Read raw .enc blob from disk
  2. Base64-encode it
  3. Compute SHA-256 hash of raw bytes
  4. POST /sync/push {project_id, env_name, blob, blob_hash}

Server:
  1. Validate JWT / API token
  2. Check user has push scope for this project/env
  3. Validate blob starts with "S1YA" (base64 of KV\x00 magic)
  4. Increment version counter (or start at 1)
  5. Store blob + hash + version + metadata
  6. Log to audit_log
  7. Return {version, pushed_at}

Client:
  1. Update local config.json with sync state
```

### Pull flow
```
Client:
  1. GET /sync/pull?project_id=X&env_name=Y

Server:
  1. Validate JWT / API token
  2. Check user has pull scope
  3. Return {blob, version, blob_hash, pushed_at}

Client:
  1. Base64-decode blob
  2. Write raw .enc file to disk (atomic write)
  3. Update local sync state
```

### Conflict detection
Last-write-wins with version numbers. The client can compare local
blob_hash with remote blob_hash to detect changes before pushing.
No merge logic — encrypted blobs can't be merged (the server can't
see inside them). The last push wins.

For teams: coordinate who pushes. The audit log shows who pushed last.
For CI/CD: tokens are typically pull-only, so no conflicts.

## Security Measures

### Rate limiting
- Auth endpoints: 5 requests per minute per IP
- Sync endpoints: 60 requests per minute per user
- Team endpoints: 20 requests per minute per user

### Input validation
- Email: RFC 5322 format, max 254 chars
- Password: min 8 chars (server-side), further strength checks optional
- project_id: alphanumeric + hyphens, max 128 chars
- env_name: alphanumeric + hyphens, max 64 chars
- blob: max 1MB base64 (decodes to ~750KB — far more than any vault needs)
- blob must start with "S1YA" (base64 of KV\x00 — validates it's an actual .enc file)

### Server-side audit
Every push/pull/invite/revoke is logged with:
- user_id, action, project_id, env_name, IP address, timestamp
- No blob content or secret values — metadata only

### Token security
- API tokens stored as bcrypt hashes (not plaintext)
- Shown once at creation, never again
- Prefix stored for identification without full comparison
- Expiry enforced server-side

## Implementation Plan

### Phase 1 — Core (MVP)
```
kv-server/
  wrangler.toml         — Cloudflare Workers config
  src/
    index.ts            — Router + middleware
    auth.ts             — Register, login, refresh
    sync.ts             — Push, pull, status
    db.ts               — D1 queries
    jwt.ts              — Token signing/verification
    middleware.ts        — Auth, rate limit, CORS
  schema.sql            — D1 schema
  package.json
```

Endpoints: /auth/*, /sync/*, /health
Auth: JWT + refresh tokens
Storage: D1 (blobs as TEXT columns, base64)
Deploy: `wrangler deploy` → live on kvsecure.com/api

### Phase 2 — Teams
Add: /team/*, team ownership, member management
Add: blob access scoped to team membership
Add: server-side audit logging

### Phase 3 — CI/CD Tokens
Add: /tokens/*
Add: scoped API tokens (pull/push/admin)
Add: project and environment restrictions

### Phase 4 — Billing
Add: Stripe Checkout integration
Add: team size limits on free tier (if applicable)
Add: usage tracking

## What Makes This World Class

1. **Structurally zero-knowledge** — the server code has NO decryption
   capability. Not policy — architecture. Even with full DB access,
   an attacker gets ciphertext that's infeasible to break.

2. **Client code already exists** — `auth.py`, `remote.py`, `sync.py`,
   `cli_remote.py` are tested and ready. The server just implements
   the other side of the contract.

3. **Edge deployment** — Cloudflare Workers run at 300+ locations.
   Sub-50ms latency for sync operations worldwide. No cold starts
   for Workers on the free plan.

4. **Blob validation** — server validates the .enc magic bytes
   before storing. Prevents garbage data and ensures the blob is
   actually a kv-encrypted file.

5. **Scoped CI/CD tokens** — read-only tokens restricted to specific
   projects and environments. A GitHub Actions token that can only
   pull staging secrets can't access prod. Granularity that Doppler
   charges $21/user for.

6. **Server-side audit trail** — who pushed what, when, from where.
   Combined with the client-side tamper-evident audit log, you get
   full provenance of every secret change.

7. **Free tier that matters** — not a teaser. Full sync for individual
   users. Teams are the paid boundary, not sync itself.

## Decisions To Validate With Satish

1. **TypeScript (Workers) vs Python (FastAPI)?** Workers requires
   TypeScript. The client is Python. Different languages for client
   and server. Acceptable?

2. **D1 vs external PostgreSQL?** D1 is simpler and free. Postgres
   gives more power. At current scale, D1 is the right choice.

3. **Team key sharing model?** Current: shared symmetric key via
   kvkey_ tokens (out-of-band). Future: per-member asymmetric
   encryption (like Bitwarden). Start with symmetric?

4. **Free tier limits?** How many projects/environments/blobs
   before requiring a team subscription?

5. **Domain:** API at `api.kvsecure.com` or `kvsecure.com/api`?

## What NOT To Build (Yet)

- Real-time sync (WebSocket push notifications)
- Merge conflict resolution (blobs are opaque — can't merge)
- Per-member encryption (symmetric key sharing is fine for v1)
- Admin dashboard (CLI is the interface)
- Mobile app / web vault
- SSO / SAML / OIDC

## Sources

- [Bitwarden Zero-Knowledge Architecture](https://bitwarden.com/resources/zero-knowledge-encryption/)
- [Bitwarden Security Whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
- [Cloudflare D1 Documentation](https://developers.cloudflare.com/d1/)
- [Cloudflare Workers Storage Options](https://developers.cloudflare.com/workers/platform/storage-options/)
- [Passbolt E2EE Architecture](https://deepwiki.com/passbolt/passbolt-docs/3.2-end-to-end-encryption-architecture)
