> ## 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.

# AWS Lambda

> Deploy the BlindCast key server as an AWS Lambda function behind API Gateway HTTP API.

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:

```bash theme={null}
pnpm add @blindcast/keys
```

Create your Lambda handler:

```typescript theme={null}
// 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

```typescript theme={null}
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:

```yaml theme={null}
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"
```

<Warning>
  Never store `MASTER_KEY` or `SALT` as plaintext in your template. Use AWS Secrets Manager, Systems Manager Parameter Store, or Lambda encrypted environment variables.
</Warning>

## 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:

```bash theme={null}
pnpm add pg
```

```typescript theme={null}
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

<Warning>
  **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.
</Warning>

* 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:

```typescript theme={null}
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
