SvelteKit adapter for AWS Lambda — deploy to Node.js or Bun runtimes, bundled with esbuild or Bun, behind CloudFront.
By default the construct uses a Lambda Function URL as the CloudFront origin (with response streaming). You can override this with any custom origin via the toDefaultOrigin prop — for example, an HTTP API Gateway when you need a Lambda authorizer.
Three build/runtime configurations are supported:
| Option | Build tool | Lambda runtime |
|---|---|---|
| 1 (default) | esbuild | Node.js |
| 2 | Bun | Bun (custom layer) |
| 3 | Bun | Node.js |
npm i kit-on-lambda aws-cdk aws-cdk-lib constructs
# or
bun i kit-on-lambda aws-cdk aws-cdk-lib constructsTo access the raw AWS event and context from inside your SvelteKit handlers, also install:
npm i @beesolve/lambda-fetch-api
# or
bun i @beesolve/lambda-fetch-api@beesolve/lambda-fetch-api provides getAwsEvent() / getAwsContext() backed by AsyncLocalStorage.
SvelteKit is deployed to AWS Lambda behind CloudFront. Static assets are served from an S3 bucket.
graph LR
Client --> CloudFront
CloudFront -->|static assets| S3[S3 Bucket]
CloudFront -->|dynamic requests| FnUrl[Function URL]
FnUrl --> Lambda[Lambda - SvelteKit]
The default adapter. Uses esbuild to bundle the server and deploys to the official Node.js Lambda runtime.
// svelte.config.jsimport{vitePreprocess}from"@sveltejs/vite-plugin-svelte";importadapterfrom"kit-on-lambda";constoriginUrl="https://{distributionId}.cloudfront.net";/** @type {import('@sveltejs/kit').Config} */constconfig={preprocess: vitePreprocess(),kit: {adapter: adapter(),paths: {assets: originUrl,},csrf: {trustedOrigins: [originUrl],},},};exportdefaultconfig;Note
Set kit.paths.assets and kit.csrf.trustedOrigins to your CloudFront distribution URL.
// app.tsimport{SvelteKit}from"kit-on-lambda/cdk";import{App,Stack,typeEnvironment}from"aws-cdk-lib";constenv: Environment={account: "your-account-id",region: "your-preferred-region",};constapp=newApp();conststack=newStack(app,"YourSite",{ env });const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "node",});Add the CDK script to your package.json:
{
"scripts": {
"dev": "vite dev",
"build": "vite build",
"cdk": "cdk --app \"node --experimental-strip-types app.ts\" --profile {your-aws-profile}"
}
}If you are using bun instead of node:
{
"scripts": {
"dev": "bun run --bun --env-file=./.env vite dev",
"build": "bunx --bun vite build",
"cdk": "cdk --app \"bun app.ts\" --profile {your-aws-profile}"
}
}Deploy:
bun run build
bun run cdk bootstrap # only needed the first time
bun run cdk deployBy default the Lambda uses InvokeMode.RESPONSE_STREAM. To use buffered responses:
const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "node",invokeMode: InvokeMode.BUFFERED,});Install @beesolve/lambda-fetch-api and use getAwsEvent() / getAwsContext() from anywhere inside a request handler. These are backed by AsyncLocalStorage — no request argument needed.
// hooks.server.tsimporttype{Handle}from"@sveltejs/kit";import{getAwsContext,getAwsEvent,isAPIGatewayProxyEvent,isAPIGatewayProxyEventV2,}from"@beesolve/lambda-fetch-api";exportconsthandle: Handle=async({ event, resolve })=>{constawsEvent=getAwsEvent();constawsContext=getAwsContext();if(isAPIGatewayProxyEvent(awsEvent)){// API Gateway v1 (REST API)}if(isAPIGatewayProxyEventV2(awsEvent)){// API Gateway v2 / Function URL}awsContext.getRemainingTimeInMillis();returnawaitresolve(event);};Uses Bun to bundle the server and deploys to a custom Bun Lambda runtime via @beesolve/lambda-bun-runtime.
// svelte.config.jsimport{vitePreprocess}from"@sveltejs/vite-plugin-svelte";importadapterfrom"kit-on-lambda/bun";constoriginUrl="https://{distributionId}.cloudfront.net";/** @type {import('@sveltejs/kit').Config} */constconfig={preprocess: vitePreprocess(),kit: {adapter: adapter({runtime: "bun"}),paths: {assets: originUrl,},csrf: {trustedOrigins: [originUrl],},},};exportdefaultconfig;// app.tsimport{SvelteKit}from"kit-on-lambda/cdk";import{App,Stack,typeEnvironment}from"aws-cdk-lib";constapp=newApp();conststack=newStack(app,"YourSite",{env: {account: "your-account-id",region: "your-preferred-region"},});const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "bun",});By default the Lambda uses InvokeMode.RESPONSE_STREAM. To use buffered responses:
const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "bun",invokeMode: InvokeMode.BUFFERED,});Uses Bun as the bundler but targets the official Node.js Lambda runtime. Useful when you want Bun's faster build times without requiring a custom Lambda layer.
// svelte.config.jsimport{vitePreprocess}from"@sveltejs/vite-plugin-svelte";importadapterfrom"kit-on-lambda/bun";constoriginUrl="https://{distributionId}.cloudfront.net";/** @type {import('@sveltejs/kit').Config} */constconfig={preprocess: vitePreprocess(),kit: {adapter: adapter({runtime: "node"}),paths: {assets: originUrl,},csrf: {trustedOrigins: [originUrl],},},};exportdefaultconfig;// app.tsimport{SvelteKit}from"kit-on-lambda/cdk";import{App,Stack,typeEnvironment}from"aws-cdk-lib";constapp=newApp();conststack=newStack(app,"YourSite",{env: {account: "your-account-id",region: "your-preferred-region"},});const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "node",});The SvelteKit construct uses a Function URL as the CloudFront origin by default. You can replace it with any origin by providing a toDefaultOrigin factory function.
Use this when you need a Lambda authorizer at the gateway level (e.g., for session validation). Response streaming is not available with HTTP API Gateway — the Lambda always uses the buffered handler.
// app.tsimport{SvelteKit}from"kit-on-lambda/cdk";import{App,Stack,typeEnvironment}from"aws-cdk-lib";import{InvokeMode}from"aws-cdk-lib/aws-lambda";import{HttpApi,HttpMethod}from"aws-cdk-lib/aws-apigatewayv2";import{HttpLambdaIntegration}from"aws-cdk-lib/aws-apigatewayv2-integrations";import{HttpOrigin}from"aws-cdk-lib/aws-cloudfront-origins";constapp=newApp();conststack=newStack(app,"YourSite",{env: {account: "your-account-id",region: "your-preferred-region"},});constapi=newHttpApi(stack,"Api");const{ handler, distribution }=newSvelteKit(stack,"SvelteKit",{runtime: "node",invokeMode: InvokeMode.BUFFERED,toDefaultOrigin: ({ handler })=>{constintegration=newHttpLambdaIntegration("Integration",handler);api.addRoutes({path: "/{proxy+}",methods: [HttpMethod.ANY],
integration,});api.addRoutes({path: "/",methods: [HttpMethod.ANY],
integration,});constapiUrl=newURL(api.apiEndpoint);returnnewHttpOrigin(apiUrl.hostname);},});When using @beesolve/auth-service or similar, the service can wire up routing with its internal authorizer:
import{SvelteKit}from"kit-on-lambda/cdk";import{Auth}from"@beesolve/auth-service/cdk";import{Fn}from"aws-cdk-lib";import{HttpOrigin}from"aws-cdk-lib/aws-cloudfront-origins";constauth=newAuth(this,"Auth",{alarms: props.alarms,frontendUri: props.frontendUri,stage: props.stage,contributorInsights: false,warmer: props.warmer,allowSignUp: false,});const{ distribution }=newSvelteKit(this,"SvelteKit",{runtime: "node",toDefaultOrigin: ({ handler })=>{auth.addAuthorizedEndpoint({lambda: handler});auth.grantSdkAccess(handler);returnnewHttpOrigin(Fn.parseDomainName(auth.api.url));},});constauthBehaviour=auth.createAuthBehavior(distribution);distribution.addBehavior("/auth/*",authBehaviour.origin,authBehaviour);The construct exposes:
handler— the Lambda function.distribution— the CloudFront distribution.
AWS (both Function URL and API Gateway) does not allow unencoded / in query parameter values. SvelteKit's named form actions use ?/actionName as the query string, which gets rejected or mangled.
Workaround: encode the action parameter in your forms and hooks so the slash is sent as %2F.
Tracked upstream: sveltejs/kit#15610
This package has been inspired by various other libraries. Some code has been adapted from: