AWS Lambda
What you'll have at the end: a Vura app deployed to AWS Lambda + API Gateway using AWS SAM, with EventBridge cron triggers for task routes.
Supports: serverless, task (via EventBridge). Hot routes are not supported — see the limitation box below.
Steps
1. Install the adapter
npm install @celsian/vura-adapter-lambda
2. Configure the adapter
// vura.config.ts
import { defineConfig } from '@celsian/vura-core';
import { lambdaAdapter } from '@celsian/vura-adapter-lambda';
export default defineConfig({
adapter: lambdaAdapter({
region: 'us-east-1',
memory: 256,
timeout: 30,
stackName: 'my-vura-app',
runtime: 'nodejs22.x',
architecture: 'arm64',
}),
});
All options have defaults: region is us-east-1, memory is 256, timeout is 30 seconds, stackName is then-app, runtime is nodejs22.x, architecture is arm64.
3. Build
npm run build
Emitted artifacts:
dist/
lambda/
api_hello_get/
index.js
route.js
api_orders_post/
index.js
route.js
task_api_cleanup/ ← task route with EventBridge cron
index.js
route.js
__pages/ ← every page, in one function
index.js
pages.js ← only when the project has `server` pages
assets/ ← prerendered pages, client bundles, public/
template.yaml
samconfig.toml
4. The SAM template
dist/template.yaml is generated by vura build. For a project with one
serverless GET route (/api/hello), one task route (/api/cleanup, schedule
0 3 * * *), and two pages (/ static, /posts server):
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Vura Application
Globals:
Function:
Runtime: nodejs22.x
Architectures:
- arm64
MemorySize: 256
Timeout: 30
Environment:
Variables:
NODE_ENV: production
Resources:
ThenHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: prod
GETApihelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
CodeUri: lambda/api_hello_get/
Events:
Api:
Type: HttpApi
Properties:
ApiId: !Ref ThenHttpApi
Path: /api/hello
Method: GET
TaskCleanupFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
CodeUri: lambda/task_api_cleanup/
Timeout: 60
Events:
Schedule:
Type: Schedule
Properties:
Schedule: cron(0 3 * * ? *)
Enabled: true
VuraPagesFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
CodeUri: lambda/__pages/
Events:
PageRoot:
Type: HttpApi
Properties:
ApiId: !Ref ThenHttpApi
Path: /
Method: GET
PagePosts:
Type: HttpApi
Properties:
ApiId: !Ref ThenHttpApi
Path: /posts
Method: GET
CatchAll:
Type: HttpApi
Properties:
ApiId: !Ref ThenHttpApi
Path: /{proxy+}
Method: ANY
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub "https://${ThenHttpApi}.execute-api.${AWS::Region}.amazonaws.com/prod"
Notes:
Runtime,Architectures,MemorySizeandTimeoutcome fromGlobalsand are not repeated per function — SAM lints duplicate values (E3032/E3037). A function overridesTimeoutonly when its route asks for a different one.- Cron expressions are converted from standard five-field format to AWS
EventBridge six-field format (a
?wildcard is inserted for the day-of-week or day-of-month field as required by EventBridge). CatchAllis last in specificity, not in effect: HTTP API matches the most specific route first, so/api/helloand/postsstill reach their own functions.
5. Validate and deploy
# Validate the template
sam validate --template dist/template.yaml --lint
# Build Lambda packages (resolves dependencies into zip bundles)
sam build --template dist/template.yaml
# Deploy (guided first time — sets up samconfig.toml answers)
sam deploy --guided
After the first --guided deploy, subsequent deploys use samconfig.toml:
sam deploy
6. Verify
# Get the API URL from the SAM output
aws cloudformation describe-stacks \
--stack-name my-vura-app \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \
--output text
curl -fsS <api-url>/api/hello
Smoke test
API_URL=$(aws cloudformation describe-stacks \
--stack-name my-vura-app \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" \
--output text)
# API route
curl -fsS "${API_URL}/api/hello"
# Prerendered page, served from the pages function's asset copy
curl -fsS "${API_URL}/" | grep -q '<h1'
# Server-mode page: rendered per request, with its loader
curl -fsS "${API_URL}/posts" | grep -q '__VURA_LOADER__'
The repo ships all of this as one script, which is what CI runs against every target:
node scripts/assert-served-pages.mjs "$API_URL"
Limitation: hot routes are not supported
Lambda terminates the process between invocations and cannot hold a WebSocket connection open. Hot routes require a persistent process.
When hot routes are present,
vura buildwarns at build time:[vura] N hot route(s) cannot run on lambda and were not bundled: /api/live/room — deploy them to a persistent host (see /self-host/)Hot routes are not silently excluded — they are named in the warning. Deploy hot routes to a persistent host (Node / VPS, Docker, or Fly.io) alongside your Lambda deployment.
Pages
All four page modes are served, by one function: VuraPagesFunction, declared
in the SAM template with a GET route per page pattern plus a greedy
/{proxy+} on ANY. HTTP API matches the most specific route first, so every
API route still wins; the catch-all serves the client bundles under /_then/,
anything from public/, and turns an unknown path into the 404 page instead of
API Gateway's 403 Missing Authentication Token.
Prerendered assets are served from the function, not from S3. They are
copied into dist/lambda/__pages/assets/ and read from /var/task. That is a
deliberate trade: sam deploy uploads code, not site content, so an
S3 + CloudFront story needs a bucket, a distribution and an aws s3 sync you
run yourself. Serving from the bundle keeps sam deploy as the whole deploy,
and costs you every asset byte as function time.
vura build tells you when that trade stops being the right one:
[vura] 31.4 MB of prerendered assets are bundled into the pages Lambda and served through it. That works, but every byte is billed as function time and inflates cold starts — put the files in S3 behind CloudFront and route only server-mode pages here.
and names any single file API Gateway cannot return at all:
[vura] 1 asset(s) exceed what API Gateway can return (6 MB, less base64 overhead) and will fail with a 500 when requested: /video/intro.mp4. Serve them from S3/CloudFront instead.
Limitation: streaming pages are buffered
A page with
export const page = { streaming: true }still renders correctly, but the shell cannot go out before the body: API Gateway's proxy integration buffers the whole response. Named at build time:[vura] N streaming page(s) are buffered on AWS Lambda — API Gateway's proxy integration has no early flush, so the shell cannot go out before the body: /feed. They still render correctly.
Limitation: a
serverpage cannot import a Node built-inThe pages bundle is built runtime-neutral so the same artifact runs on every serverless target. A
serverpage, layout or loader that imports anode:module fails the build rather than being dropped:[vura] server-mode page(s) could not be bundled for AWS Lambda: src/pages/report.tsx.Move that work into an API route the page fetches with its loader.
revalidateTag inside Lambda functions
Lambda function bundles include a revalidateTag/revalidatePath shim that logs a warning instead of calling a local cache engine:
[vura] revalidateTag("posts") is a no-op inside Lambda functions today — call your cache host's /__vura/revalidate webhook instead.
Lambda functions have no local ISR cache. To invalidate ISR cache from a Lambda function, make an authenticated POST to your Node server's /__vura/revalidate endpoint:
curl -X POST https://your-node-server.com/__vura/revalidate \
-H "x-vura-revalidate-secret: your-secret" \
-H "Content-Type: application/json" \
-d '{"tags":["posts"]}'
Cold-start latency has not been measured for this adapter — no numbers are stated here. Measure for your own function sizes and regions before making performance claims.
CI-tested: this guide is verified by the
lambdajob in.github/workflows/selfhost.yml. The job builds the project with the Lambda adapter, runssam validate --lintondist/template.yaml, and boots the handler undersam local start-api(Docker-backed Lambda emulation), then runsscripts/assert-served-pages.mjsagainst it: every page mode, the browser bundle a client page boots from, the loader payload on aserverpage (twice, a second apart, to prove the loader runs per request), the API route, and the 404. It does not deploy to AWS — no cloud credentials are in CI; API Gateway, IAM, and real Lambda networking behavior are out of scope.
Route kind support
| Kind | Supported |
|---|---|
| Serverless | yes |
| Hot (WebSocket) | no — build warns by name |
| Task (cron) | yes — via EventBridge Schedule event |