Skip to main content
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 serverAWS Lambda
Centralized deploymentServerless, scales to zero
SQLite or Postgres for leasesPostgres or your own LeaseStore
Runs anywhere Docker runsRuns on AWS
Bundled presign endpointCustom presign route (or separate Lambda)
Best for: most deploymentsBest for: AWS-native stacks, pay-per-request

Quick start

Install the keys package:
pnpm add @blindcast/keys
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:
MethodPathDescription
GET/keys/:contentIdContent key (16 raw bytes)
GET/keys/:contentId/:epochEpoch key (for key rotation)
POST/keys/leasesCreate a lease (when leaseStore is configured)
POST/keys/leases/renewRenew 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:
pnpm add pg
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

FeatureDockerLambda
Key derivationHKDF-SHA-256HKDF-SHA-256 (identical)
AuthenticationJWT via env varsCustom authenticate callback
LeasesSQLite / PostgresPostgres (or your own LeaseStore)
Presign endpointBuilt-inCustom route or separate Lambda
ConfigurationEnvironment variablesEnvironment variables (same)
Cold startsNone~100ms (Node.js 20)
Deploymentdocker runSAM / 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