The key server can run on Cloudflare Workers instead of Docker. Workers deploy to 300+ edge locations, giving viewers sub-50ms key fetch latency worldwide — without managing containers.
When to use Workers
| Docker key server | Cloudflare Workers |
|---|
| Centralized deployment | Global edge deployment |
| SQLite or Postgres for leases | KV or Durable Objects for leases |
| Runs anywhere Docker runs | Runs on Cloudflare’s network |
| Bundled presign endpoint | No presign (use a separate API) |
| Best for: most deployments | Best for: global latency-sensitive apps |
Quick start
Install the keys package:
Create your Worker:
// src/index.ts
import { createWorkerKeyServer } from "@blindcast/keys"
const handler = createWorkerKeyServer({
masterKey: env.MASTER_KEY_HEX,
salt: env.SALT_HEX,
corsOrigins: "https://your-app.com",
})
export default { fetch: handler }
Deploy with Wrangler:
Full example with authentication
import { createWorkerKeyServer } from "@blindcast/keys"
const handler = createWorkerKeyServer({
masterKey: env.MASTER_KEY_HEX,
salt: env.SALT_HEX,
corsOrigins: "https://your-app.com",
authenticate: async (request, env) => {
const token = request.headers.get("Authorization")?.split(" ")[1]
if (!token) {
return { ok: false, status: 401, reason: "Missing token" }
}
// Verify JWT (use your preferred JWT library)
const valid = await verifyJwt(token, env.JWT_SECRET)
if (!valid) {
return { ok: false, status: 401, reason: "Invalid token" }
}
return { ok: true }
},
})
export default { fetch: handler }
Wrangler configuration
# wrangler.toml
name = "blindcast-keyserver"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
CORS_ORIGINS = "https://your-app.com"
# Store secrets with: npx wrangler secret put MASTER_KEY_HEX
# Store secrets with: npx wrangler secret put SALT_HEX
Never put MASTER_KEY_HEX or SALT_HEX in wrangler.toml. Use wrangler secret put to store them as encrypted secrets.
Endpoints
The Worker key server exposes:
| Method | Path | Description |
|---|
GET | /keys/:contentId | Content key (16 raw bytes) |
GET | /keys/:contentId/:epoch | Epoch key (for key rotation) |
GET | /health | Health check |
The Worker key server does not include lease or presign endpoints. For leases, implement a KV-backed LeaseStore (see below). For presigned uploads, use a separate API endpoint.
Leases on Workers
The in-memory LeaseStore does not work on Workers — each isolate has independent memory and does not share state across requests. Use Cloudflare KV or Durable Objects instead.
KV-backed lease store
import { createWorkerKeyServer } from "@blindcast/keys"
import type { LeaseStore } from "@blindcast/keys"
function createKVLeaseStore(kv: KVNamespace): LeaseStore {
return {
async create(leaseId, viewerId, contentId, expiresAt) {
await kv.put(`lease:${leaseId}`, JSON.stringify({
viewerId, contentId, expiresAt: expiresAt.toISOString(), revoked: false,
}), { expirationTtl: Math.ceil((expiresAt.getTime() - Date.now()) / 1000) })
},
async validate(leaseId, contentId) {
const raw = await kv.get(`lease:${leaseId}`)
if (!raw) return { ok: false, code: "LEASE_NOT_FOUND" }
const lease = JSON.parse(raw)
if (lease.revoked) return { ok: false, code: "LEASE_REVOKED" }
if (new Date(lease.expiresAt) < new Date()) return { ok: false, code: "LEASE_EXPIRED" }
if (lease.contentId !== contentId) return { ok: false, code: "LEASE_CONTENT_MISMATCH" }
return { ok: true }
},
async revoke(leaseId) {
const raw = await kv.get(`lease:${leaseId}`)
if (!raw) return
const lease = JSON.parse(raw)
lease.revoked = true
await kv.put(`lease:${leaseId}`, JSON.stringify(lease))
},
// Additional methods: renew, revokeByViewer, cleanup
}
}
Add the KV binding to wrangler.toml:
[[kv_namespaces]]
binding = "LEASES"
id = "your-kv-namespace-id"
Differences from Docker
| Feature | Docker | Workers |
|---|
| Key derivation | HKDF-SHA-256 | HKDF-SHA-256 (identical) |
| Authentication | JWT via env vars | Custom authenticate callback |
| Leases | SQLite / Postgres | KV / Durable Objects |
| Presign endpoint | Built-in | Not included |
| Database | Auto-migrated | Not applicable |
| Health check | GET /health | GET /health |
| Deployment | docker run | wrangler deploy |
Player configuration
The player connects to a Worker key server the same way as Docker — just point to the Worker URL:
import { createPlayer } from "@blindcast/player"
const player = createPlayer(videoElement, {
keyServerUrl: "https://blindcast-keyserver.your-account.workers.dev/keys",
// Everything else is identical
})
When to choose Docker instead
Use the Docker key server when:
- You need the bundled presign endpoint for browser uploads
- You want SQLite or Postgres lease storage without custom code
- You’re already running containers and don’t need edge latency
- You need all endpoints in a single deployment