Deploying to AWS Lambda
AWS Lambda is a first-class PlumeKit runtime, alongside the native SwiftNIO server and the Cloudflare Worker. The same buildApp() runs on all three: you change plumekit.toml (and the deploy config) and rebuild, with no app-code change and no if platform == … branch. On AWS your app runs as a provided.al2 custom runtime, fronted by API Gateway, with every host capability supplied by an AWS service behind the existing capability protocols.
This page covers the AWS adapters, the [targets.aws] manifest profile, the environment it reads, packaging with plumekit build --target aws, deploying to Lambda + API Gateway and local testing against LocalStack.
Three runtimes, one core
Each runtime is an adapter module and a way of shipping the same app:
| Runtime | Adapter module | How you ship it |
|---|---|---|
| Native (SwiftNIO) | PlumeServer | plumekit serve (long-lived process) |
| Cloudflare Worker | PlumeWorker | plumekit build --target cloudflare (Wasm) |
| AWS Lambda | PlumeAWS | plumekit build --target aws (provided.al2) |
App code names a capability (request.bindings.database, request.bindings.storage and so on), never a platform type. The manifest selects which adapters are linked; the generated composition root wires them into the Context your handlers receive.
The AWS adapters
Each neutral capability protocol has an AWS-service adapter. They live in PlumeAWS (SigV4 signing plus the service clients and the Lambda runtime), with PlumeS3 for object storage and PlumePostgres for the database:
| Capability | Protocol | AWS adapter |
|---|---|---|
| Object storage | StorageDriver | S3 |
| Queue | MessageQueue | SQS |
| Secrets | SecretStore | SSM Parameter Store |
| KV | KV | DynamoDB |
| Cache | Cache | DynamoDB (TTL via the table's TTL attribute) |
| Mailer | Mailer | SES |
| HTTP / fetch | HTTPClient | URLSession |
| SQL | SQLDatabase | RDS Postgres (via PlumePostgres) |
| Channels | Channel | API Gateway WebSockets (DynamoDB state + postToConnection fan-out) |
Cache reuses the DynamoDB adapter but relies on the table's TTL attribute, so expired entries are swept automatically. The contract is the same best-effort one as on every other target: a miss means "recompute", never "gone for good".
Real-time channels drive a third structurally-different runtime unchanged. The Channel protocol's synchronous-handler-over-a-pre-loaded-store shape (built for the Cloudflare Durable Object) maps cleanly onto API Gateway WebSockets: connection and per-channel state live in DynamoDB, and the adapter fans messages out with the API Gateway Management API's postToConnection. See Channels.
The [targets.aws] profile
The AWS adapter set is selected by an [targets.aws] profile in plumekit.toml, exactly like the [targets.native] and [targets.cloudflare] profiles. The PlumeKitCodegen build-tool plugin generates the composition root from it (as Composition.awsContext()) on every swift build, the direct analogue of the native Composition.nativeContext():
[targets.aws]
database = "postgres" # RDS Postgres (SQLDatabase)
storage = "s3" # S3
queue = "sqs" # SQS
secrets = "ssm" # SSM Parameter Store
channel = "apigateway" # API Gateway WebSockets + DynamoDBThe [capabilities] table (shared by every target) still gates which typed request.bindings accessors exist. Declaring a capability you don't wire, or using one you didn't declare, is a compile error, on AWS the same as everywhere else.
Configuration (environment)
Config is runtime, read from the process environment (which on Lambda is the function configuration), never compiled in. The key variables:
| Variable | Purpose |
|---|---|
AWS_REGION | Region for every service client. |
AWS_ACCESS_KEY_ID | SigV4 credentials (or the Lambda execution role's injected creds). |
AWS_SECRET_ACCESS_KEY | SigV4 credentials. |
AWS_ENDPOINT_URL | Overrides all service endpoints; set to http://localhost:4566 for LocalStack. |
DATABASE_URL | RDS Postgres connection string. |
S3_BUCKET | Object-storage bucket. |
KV_TABLE | DynamoDB table backing the KV capability. |
CACHE_TABLE | DynamoDB table backing the cache (with a TTL attribute). |
SQS_URL | Queue URL for the SQS producer. |
CHANNEL_TABLE | DynamoDB table holding channel connections + state. |
CHANNEL_MGMT_ENDPOINT | API Gateway Management API endpoint used for postToConnection. |
Because AWS_ENDPOINT_URL overrides every service endpoint at once, the same binary targets real AWS or a local LocalStack simply by setting (or unsetting) that one variable.
Packaging: plumekit build --target aws
Build the AWS bundle from the app directory (or by naming one):
$plumekit build --target aws # from the app directory$plumekit build --target aws path/to/appThis produces dist/aws/:
dist/aws/
├── bootstrap # the entrypoint binary (provided.al2 expects this name)
├── function.zip # bootstrap zipped, ready to upload as the function code
├── public/ # your Public/ static files, copied for S3 + CloudFront
└── README.md # the generated deploy notesLambda's provided.al2 custom runtime looks for an executable named bootstrap. That binary is the app's Lambda entry point: LambdaAdapter.run polls the Lambda Runtime API and maps API Gateway proxy events to PlumeKit Request/Response and back. Both API Gateway flavours are handled: HTTP API v2 (payload format 2.0) and REST API v1 (payload format 1.0).
Linux SDK for a real deploy
Lambda runs Linux, so a real deploy needs a Linux binary. Point PLUMEKIT_LINUX_SDK at a static Linux Swift SDK (install one from swift.org, or use the static-Linux SDK) and rebuild:
$export PLUMEKIT_LINUX_SDK=<swift-linux-sdk-id>$plumekit build --target awsWithout PLUMEKIT_LINUX_SDK the CLI builds with the host toolchain. That is fine for driving the Lambda locally as a plain process (e.g. against LocalStack), but the resulting bootstrap will not run on Lambda itself.
Deploying
- Provision the services the
[targets.aws]profile references: an S3 bucket, the SQS queue, SSM parameters for your secrets, the DynamoDB tables (KV, cache with a TTL attribute and channel state), an RDS Postgres instance, and the SES identity. - Create the function on the
provided.al2runtime withfunction.zipas the code andbootstrapas the handler. Set the environment variables from the table above; the execution role supplies the AWS credentials. - Front it with API Gateway. An HTTP API (v2) or REST API (v1) proxies all routes to the function. For real-time channels, add a WebSocket API and give the function
execute-api:ManageConnectionssopostToConnectioncan fan out; pointCHANNEL_MGMT_ENDPOINTat that API's management endpoint. - Run migrations against RDS Postgres with
plumekit migrate(migrations are native-only; they run from the CLI or a build step, never inside the function). The dialect travels with the handle, so the same migrations that run on SQLite/D1 applySERIAL-style DDL on Postgres. See Migrations.
Static files (Public/)
The build copies your app's Public/ directory (styles, images and the content-hashed Plume bundle app.) to dist/aws/public. The Lambda does not serve static files: upload them to S3 and front them with CloudFront, which routes the asset paths to S3 and every other path to API Gateway. The generated dist/aws/README.md has the exact aws s3 sync command and CloudFront setup; Portability is the canonical guide. For runtime* uploads (avatars, exports) served from S3 rather than static files, use Storage.serve.
Local testing with LocalStack
You can exercise the whole AWS runtime locally with LocalStack, with no AWS account. The one switch is AWS_ENDPOINT_URL: set it to http://localhost:4566 and every service client talks to LocalStack instead of real AWS.
A local run of your app looks like this: boot LocalStack (plus a Postgres container for the database), provision the resources your [targets.aws] profile needs (the bucket, the queue, SSM parameters and the DynamoDB tables) with the aws CLI pointed at the local endpoint, then build with the host toolchain (no PLUMEKIT_LINUX_SDK) and run the resulting bootstrap with AWS_ENDPOINT_URL and the resource variables from the table above set. The binary polls the Lambda Runtime API (AWS_LAMBDA_RUNTIME_API), so drive it through a local Runtime API emulator such as AWS's Lambda Runtime Interface Emulator.
S3, SQS, SSM and DynamoDB run on LocalStack, so storage, queue, secrets, KV, cache and the API Gateway channel store are all exercised locally against the same adapters you deploy.
See also
- Deploying:
plumekit deploy, CI and containers; AWS is one of its targets. - Portability: how the one-core/many-adapters invariant holds across native, Cloudflare and AWS.
- Bindings & the capability model: the capability protocols and the per-target adapter table (native, Cloudflare, AWS).
- Channels: the platform-neutral real-time protocol driving every target.
- Migrations: hand-authored migrations applied against RDS Postgres.