Skip to main content
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 serverCloudflare Workers
Centralized deploymentGlobal edge deployment
SQLite or Postgres for leasesKV or Durable Objects for leases
Runs anywhere Docker runsRuns on Cloudflare’s network
Bundled presign endpointNo presign (use a separate API)
Best for: most deploymentsBest for: global latency-sensitive apps

Quick start

Install the keys package:
pnpm add @blindcast/keys
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:
npx wrangler deploy

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:
MethodPathDescription
GET/keys/:contentIdContent key (16 raw bytes)
GET/keys/:contentId/:epochEpoch key (for key rotation)
GET/healthHealth 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

FeatureDockerWorkers
Key derivationHKDF-SHA-256HKDF-SHA-256 (identical)
AuthenticationJWT via env varsCustom authenticate callback
LeasesSQLite / PostgresKV / Durable Objects
Presign endpointBuilt-inNot included
DatabaseAuto-migratedNot applicable
Health checkGET /healthGET /health
Deploymentdocker runwrangler 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