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.
The key server can run on AWS Lambda behind API Gateway. Lambda scales to zero when idle and scales automatically under load — no containers to manage.
When to use Lambda
| Docker key server | AWS Lambda |
|---|
| Centralized deployment | Serverless, scales to zero |
| SQLite or Postgres for leases | Postgres or your own LeaseStore |
| Runs anywhere Docker runs | Runs on AWS |
| Bundled presign endpoint | Custom presign route (or separate Lambda) |
| Best for: most deployments | Best for: AWS-native stacks, pay-per-request |
Quick start
Install the keys package:
Create your Lambda handler:
// handler.ts
import { createLambdaKeyServer } from "@blindcast/keys/lambda"
import { hexToBytes } from "@blindcast/crypto"
export const handler = createLambdaKeyServer({
masterKey: hexToBytes(process.env.MASTER_KEY!),
salt: hexToBytes(process.env.SALT!),
corsOrigins: process.env.CORS_ORIGINS!,
})
Deploy with SAM, CDK, or the Serverless Framework.
Full example with authentication
import { createLambdaKeyServer } from "@blindcast/keys/lambda"
import { hexToBytes } from "@blindcast/crypto"
export const handler = createLambdaKeyServer({
masterKey: hexToBytes(process.env.MASTER_KEY!),
salt: hexToBytes(process.env.SALT!),
corsOrigins: process.env.CORS_ORIGINS!,
authenticate: async (event) => {
const token = event.headers?.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, process.env.JWT_SECRET!)
if (!valid) {
return { ok: false, status: 401, reason: "Invalid token" }
}
return { ok: true }
},
})
SAM template
Minimal template.yaml for API Gateway HTTP API + Lambda:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
Timeout: 10
MemorySize: 128
Resources:
KeyServerFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handler.handler
CodeUri: ./dist
Environment:
Variables:
CORS_ORIGINS: !Ref CorsOrigins
# MASTER_KEY and SALT are resolved from Secrets Manager at runtime
MASTER_KEY: !Sub "{{resolve:secretsmanager:blindcast/keys:SecretString:masterKey}}"
SALT: !Sub "{{resolve:secretsmanager:blindcast/keys:SecretString:salt}}"
Events:
KeysApi:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
Parameters:
CorsOrigins:
Type: String
Default: "https://your-app.com"
Outputs:
KeyServerUrl:
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
Never store MASTER_KEY or SALT as plaintext in your template. Use AWS Secrets Manager, Systems Manager Parameter Store, or Lambda encrypted environment variables.
Endpoints
The Lambda key server exposes:
| Method | Path | Description |
|---|
GET | /keys/:contentId | Content key (16 raw bytes) |
GET | /keys/:contentId/:epoch | Epoch key (for key rotation) |
POST | /keys/leases | Create a lease (when leaseStore is configured) |
POST | /keys/leases/renew | Renew a lease |
Leases on Lambda
The in-memory LeaseStore does not work across Lambda invocations — each invocation may run in a different execution context. Use the Postgres lease store instead:
import { createLambdaKeyServer } from "@blindcast/keys/lambda"
import { createPostgresLeaseStore } from "@blindcast/keys/lease-postgres"
import { hexToBytes } from "@blindcast/crypto"
const leaseStore = await createPostgresLeaseStore(process.env.DATABASE_URL!)
export const handler = createLambdaKeyServer({
masterKey: hexToBytes(process.env.MASTER_KEY!),
salt: hexToBytes(process.env.SALT!),
corsOrigins: process.env.CORS_ORIGINS!,
leaseStore,
authenticate: async (event) => {
const token = event.headers?.authorization?.split(" ")[1]
if (!token) return { ok: false, status: 401, reason: "Missing token" }
const valid = await verifyJwt(token, process.env.JWT_SECRET!)
if (!valid) return { ok: false, status: 401, reason: "Invalid token" }
return { ok: true }
},
getViewerId: (event) => {
// JWT signature already verified by authenticate() above
const token = event.headers?.authorization?.split(" ")[1]
const payload = JSON.parse(
Buffer.from(token!.split(".")[1], "base64url").toString(),
)
return payload.sub
},
})
The Postgres connection pool persists across warm Lambda invocations, so connections are reused efficiently.
You can also implement your own LeaseStore against any persistent backend — the interface is provider-agnostic.
Differences from Docker
| Feature | Docker | Lambda |
|---|
| Key derivation | HKDF-SHA-256 | HKDF-SHA-256 (identical) |
| Authentication | JWT via env vars | Custom authenticate callback |
| Leases | SQLite / Postgres | Postgres (or your own LeaseStore) |
| Presign endpoint | Built-in | Custom route or separate Lambda |
| Configuration | Environment variables | Environment variables (same) |
| Cold starts | None | ~100ms (Node.js 20) |
| Deployment | docker run | SAM / CDK / Serverless Framework |
| Import | @blindcast/keys/express | @blindcast/keys/lambda |
Security
Do not enable API Gateway’s built-in CORS when using the Lambda adapter. The adapter handles CORS internally via corsOrigins. Enabling both causes duplicate or conflicting Access-Control-Allow-Origin headers.
- Store key material in AWS Secrets Manager or Systems Manager Parameter Store — not as plaintext environment variables in your SAM/CDK template
- Use Lambda resource policies or API Gateway authorizers for additional auth layers
- Enable CloudWatch logging for key request auditing
Player configuration
The player connects to a Lambda key server the same way as Docker — just point to the API Gateway URL:
import { createPlayer } from "@blindcast/player"
const player = createPlayer(videoElement, {
keyServerUrl: "https://abc123.execute-api.us-east-1.amazonaws.com/keys",
// Everything else is identical
})
When to choose Docker instead
Use the Docker key server when:
- You want the bundled presign endpoint without writing custom code
- You want SQLite lease storage (no external database needed)
- You’re already running containers and don’t need serverless scaling
- You want to avoid Lambda cold starts