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,
getViewerId: (event) => {
// Extract viewer ID from JWT claims (use your JWT decode utility)
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