AWS Lambda
AWS Lambda runs your app's server on demand, so there is no server to set up or keep running. A Lambda function does not serve the app's static files, though, so this guide uses three AWS services together:
- AWS Lambda runs the app's server, which handles its pages and API.
- Amazon S3 stores the app's static files, such as JavaScript, CSS, and images.
- Amazon CloudFront gives the app one public address with HTTPS and sends each request to the right place.
This guide walks through deploying one standalone app from scratch. You will create the app, give it a production database and an auth secret, prepare the database, build it for Lambda, create the function, upload the static files, and then put CloudFront in front of both.
Node.js 22.22+ is required.
You also need an AWS account and the AWS CLI installed and signed in on your computer, with a default region set. Run the commands in this guide on your own computer. Steps 5 and 6 use the AWS CLI, and Step 7 uses the CloudFront console.
Step 1: Create the app
Create your new standalone app. The example below creates an app from the Chat
template, but you can start from any template. Move into the app's folder and
install its dependencies. corepack enable turns on the pnpm version the app
expects, so you do not need to install pnpm yourself.
npx --yes @agent-native/core@latest create my-app --standalone --template chat
cd my-app
corepack enable
pnpm installReplace my-app with your own app name if you like. The rest of this guide
assumes you are inside the app's folder.
If these commands fail with peer dependency errors, an outdated copy of a
package in your npx cache may be the cause. Run npx clear-npx-cache to clear
the cache, then run the commands again.
Step 2: Configure environment secrets
The app reads its production settings from environment variables:
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | Connects the app to its PostgreSQL database |
BETTER_AUTH_SECRET |
Yes | Signs user sessions |
BETTER_AUTH_URL |
Yes | Sets the public address people use to reach your app |
The exports below make the following commands work in this shell. Before
deploying, save these same variables in your host's project or service
environment settings. Step 5 saves the first two on the Lambda function, and
Step 8 adds BETTER_AUTH_URL.
Database URL
During local development the app stores data in a local database when
DATABASE_URL is unset. A deployed app needs a real PostgreSQL database that
keeps its data between deploys. Lambda does not keep files between requests, so
the database must live outside the function. Copy the connection string from
your provider and export it:
export DATABASE_URL='your-postgres-url-string'The database provider guides show where to find the connection string for each provider, including Amazon RDS for PostgreSQL.
Select a persistent PostgreSQL provider: Neon, Supabase, Amazon RDS, etc.
Auth secret
Generate this once and keep it stable across deploys.
The app uses BETTER_AUTH_SECRET to sign user sessions. If it changes, every
signed-in user is signed out. The command below generates a random value:
export BETTER_AUTH_SECRET="$(openssl rand -hex 32)"Save the generated value somewhere safe, such as a password manager. Use the same value every time you deploy.
Public URL
BETTER_AUTH_URL is the public address people use to reach your app. On most
hosts it is optional, because the app works out its address from each incoming
request. Behind CloudFront, the function receives requests addressed to its own
Lambda URL instead of your public address, so the app needs this variable to
build correct sign-in links and OAuth redirects.
You get the address when you create the CloudFront distribution in Step 7, and you set this variable in Step 8.
Step 3: Migrate
Migrations create and update the tables the app needs in your database. Run this before the first deploy:
pnpm migrate:productionThe command runs on your computer and connects to the production database with
the DATABASE_URL you exported in Step 2. Run it again after you upgrade the
app, before you deploy the new version.
Step 4: Build
Build the app for Lambda:
env -u DATABASE_URL -u BETTER_AUTH_SECRET -u BETTER_AUTH_URL NITRO_PRESET=aws-lambda pnpm buildNITRO_PRESET=aws-lambda builds the app as a Lambda function. The build splits
the output into two folders:
The .output/server folder becomes the Lambda function, and the
.output/public folder goes to S3.
The build copies the app's variables from your shell into
.output/server/.env, which ends up inside the function's deployment package.
env -u removes the secrets from the build's environment, so they stay out of
the package. Set them on the function instead, as Step 5 and Step 8 show.
Step 5: Create the function
Create the function's role
The function needs an IAM role that lets it run and write logs. Create one once for the app:
aws iam create-role --role-name my-app-lambda --assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name my-app-lambda --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
export ROLE_ARN="$(aws iam get-role --role-name my-app-lambda --query Role.Arn --output text)"Package and create the function
Zip the server folder, then create the function from the zip file:
(cd .output/server && zip -qr ../../function.zip .)
aws lambda create-function \
--function-name my-app \
--runtime nodejs24.x \
--handler server.handler \
--role "$ROLE_ARN" \
--zip-file fileb://function.zip \
--memory-size 1024 \
--timeout 60 \
--environment "$(node -e 'console.log(JSON.stringify({ Variables: { DATABASE_URL: process.env.DATABASE_URL, BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET } }))')"--handler server.handler points Lambda at the entry file from Step 4. The
node -e command turns the two secrets you exported in Step 2 into the format
the AWS CLI expects, so you do not have to paste them by hand. --timeout 60
gives each request up to 60 seconds, which matches the CloudFront limit in
Step 7.
A new role can take a few seconds to become usable. If create-function
reports that the role cannot be assumed, wait a moment and run it again.
Add a function URL
A function URL gives the function its own HTTPS address, which CloudFront uses in Step 7:
aws lambda create-function-url-config --function-name my-app --auth-type NONE
aws lambda add-permission --function-name my-app --statement-id FunctionUrlPublic --action lambda:InvokeFunctionUrl --principal "*" --function-url-auth-type NONE
aws lambda add-permission --function-name my-app --statement-id FunctionUrlInvoke --action lambda:InvokeFunction --principal "*" --invoked-via-function-urlThe first command prints the FunctionUrl, which looks like
https://abc123.lambda-url.us-east-1.on.aws/. Copy it for Step 7. The two
add-permission commands allow requests to reach the function through that
address.
Step 6: Upload the static files
Create an S3 bucket for the app's static files and copy them in. Bucket names are shared across all of AWS, so add something unique to the name:
aws s3 mb s3://my-app-assets-123456
aws s3 sync .output/public s3://my-app-assets-123456The bucket stays private. CloudFront reads from it in Step 7.
Step 7: Put CloudFront in front
Create the CloudFront distribution in the CloudFront console:
Create the distribution
Create a distribution and choose the S3 bucket from Step 6 as its origin. For Origin access, choose Origin access control settings (recommended) and create a new control setting. After the distribution is created, copy the bucket policy that the console offers and add it to the bucket's permissions in the S3 console, so CloudFront can read the files.
Add the function as a second origin
On the distribution's Origins tab, create an origin. For Origin domain, paste the function URL's host name from Step 5, without
https://or the trailing slash. Set Protocol to HTTPS only and Response timeout to 60 seconds. Do not add an origin access control to this origin.Send app requests to the function
On the Behaviors tab, edit the Default (*) behavior. Set its origin to the function, Viewer protocol policy to Redirect HTTP to HTTPS, and Allowed HTTP methods to the option that includes
POST. Set Cache policy to CachingDisabled and Origin request policy to AllViewerExceptHostHeader.Send static files to S3
Create a behavior with the path pattern
/assets/*and the S3 origin, using the CachingOptimized cache policy. Add the same kind of behavior for each top-level file in.output/public, such as*.svgand/manifest.jsonfor the Chat template.Copy the address
Wait for the distribution to finish deploying, then copy its domain name, which looks like
d111111abcdef8.cloudfront.net.
AllViewerExceptHostHeader forwards the browser's cookies, headers, and
query strings to the function, but not its Host header. The function URL only
accepts requests addressed to its own host name, which is why the app needs
BETTER_AUTH_URL in the next step.
Step 8: Set the public address and deploy
Export the CloudFront address and save all three variables on the function:
export BETTER_AUTH_URL='https://d111111abcdef8.cloudfront.net'
aws lambda update-function-configuration \
--function-name my-app \
--environment "$(node -e 'console.log(JSON.stringify({ Variables: { DATABASE_URL: process.env.DATABASE_URL, BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET, BETTER_AUTH_URL: process.env.BETTER_AUTH_URL } }))')"This command replaces all of the function's variables at once, so it includes the two from Step 5 as well. Use the same full list whenever you change a variable.
Open the CloudFront address and create the first account.
Deploy updates
To ship a new version, run the migrations, build again, upload the new function code and static files, and clear CloudFront's cache:
pnpm migrate:production
env -u DATABASE_URL -u BETTER_AUTH_SECRET -u BETTER_AUTH_URL NITRO_PRESET=aws-lambda pnpm build
rm -f function.zip && (cd .output/server && zip -qr ../../function.zip .)
aws lambda update-function-code --function-name my-app --zip-file fileb://function.zip
aws s3 sync .output/public s3://my-app-assets-123456
aws cloudfront create-invalidation --distribution-id your-distribution-id --paths "/*"aws s3 sync keeps the previous version's files in the bucket, so pages that
are already open in a browser can still load them. Find the distribution ID on
the distribution's page in the CloudFront console. The function's variables
stay in place, so you do not need to repeat Step 8.
Limits
Why is the function URL public?
CloudFront can sign requests to a function URL with origin access control, but
Lambda then requires every POST and PUT request to include a hash of its
body in an x-amz-content-sha256 header. Browsers do not send that header, so
the app's forms and actions would fail. The function URL stays public, and
CloudFront is the address you share.
How long can a request run?
CloudFront waits up to 60 seconds for the function, and the function's timeout is also 60 seconds. Agent-Native already ends a chat turn after about 40 seconds and continues it from the browser, so long agent turns still finish, but they run in several steps instead of one.
Are responses streamed?
No. The function returns each response in one piece, and a single response can be at most 6 MB.
How large can the app be?
A zip file uploaded directly to Lambda can be at most 50 MB, and the unzipped
function can be at most 250 MB. If function.zip is larger than 50 MB, upload
it to S3 first and create or update the function from there.
What's next
- Node.js - run the app on a machine you manage
- Deployment Environment Variables - the full list of production settings
- Deployment - compare hosting targets and prerequisites