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

# Database

> SQLite by default for development, Postgres for production. The database stores lease state — keys are derived, not stored.

The key server uses a database only for **lease state** (active leases, revocations). Content keys are never stored — they are derived on every request from the master key using HKDF.

## SQLite (default)

SQLite is the default. The database file is stored at `/data/blindcast.db` inside the container.

```bash theme={null}
docker run -d \
  -v blindcast-data:/data \
  -e MASTER_KEY_HEX=... \
  -e SALT_HEX=... \
  -e CORS_ORIGINS=https://app.example.com \
  -p 4100:4100 \
  blindcast/keyserver
```

<Note>
  Mount a Docker volume at `/data` to persist leases across container restarts. Without a volume, lease state is lost when the container stops.
</Note>

### When SQLite is fine

* Development and testing
* Single-instance deployments
* Low-throughput key servers (\< 100 concurrent viewers)

### When to switch to Postgres

* Multiple key server instances (SQLite doesn't support concurrent writers from different processes)
* High-throughput deployments (> 100 concurrent viewers)
* You need lease data in your existing Postgres for querying or auditing

## Postgres

Set `DATABASE_URL` to switch to Postgres:

```bash theme={null}
docker run -d \
  -e MASTER_KEY_HEX=... \
  -e SALT_HEX=... \
  -e CORS_ORIGINS=https://app.example.com \
  -e DATABASE_URL=postgres://user:pass@db.example.com:5432/blindcast \
  -p 4100:4100 \
  blindcast/keyserver
```

## Auto-migration

The key server automatically creates the required tables on startup. No manual migration step needed.

### Lease table schema

```sql theme={null}
CREATE TABLE leases (
  id TEXT PRIMARY KEY,           -- lease ID (UUID)
  viewer_id TEXT NOT NULL,       -- viewer identifier (from JWT sub claim)
  content_id TEXT NOT NULL,      -- content being accessed
  expires_at TIMESTAMP NOT NULL, -- when the lease expires
  revoked BOOLEAN DEFAULT FALSE, -- true if explicitly revoked
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_leases_viewer ON leases(viewer_id);
CREATE INDEX idx_leases_content ON leases(content_id);
CREATE INDEX idx_leases_expires ON leases(expires_at);
```

## If leases are not used

If you don't use leases (no `LEASE_TTL_MS` set, no player `lease` option), the database is not accessed at runtime. Key derivation is purely in-memory with no I/O.
