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.js
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import adapter from "kit-on-lambda";
const originUrl = "https://{distributionId}.cloudfront.net";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
paths: {
assets: originUrl,
},
csrf: {
trustedOrigins: [originUrl],
},
},
};
export default config;Note
Set kit.paths.assets and kit.csrf.trustedOrigins to your CloudFront distribution URL.
// app.ts
import { SvelteKit } from "kit-on-lambda/cdk";
import { App, Stack, type Environment } from "aws-cdk-lib";
const env: Environment = {
account: "your-account-id",
region: "your-preferred-region",
};
const app = new App();
const stack = new Stack(app, "YourSite", { env });
const { handler, distribution } = new SvelteKit(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 } = new SvelteKit(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.ts
import type { Handle } from "@sveltejs/kit";
import {
getAwsContext,
getAwsEvent,
isAPIGatewayProxyEvent,
isAPIGatewayProxyEventV2,
} from "@beesolve/lambda-fetch-api";
export const handle: Handle = async ({ event, resolve }) => {
const awsEvent = getAwsEvent();
const awsContext = getAwsContext();
if (isAPIGatewayProxyEvent(awsEvent)) {
// API Gateway v1 (REST API)
}
if (isAPIGatewayProxyEventV2(awsEvent)) {
// API Gateway v2 / Function URL
}
awsContext.getRemainingTimeInMillis();
return await resolve(event);
};Uses Bun to bundle the server and deploys to a custom Bun Lambda runtime via @beesolve/lambda-bun-runtime.
// svelte.config.js
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import adapter from "kit-on-lambda/bun";
const originUrl = "https://{distributionId}.cloudfront.net";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({ runtime: "bun" }),
paths: {
assets: originUrl,
},
csrf: {
trustedOrigins: [originUrl],
},
},
};
export default config;// app.ts
import { SvelteKit } from "kit-on-lambda/cdk";
import { App, Stack, type Environment } from "aws-cdk-lib";
const app = new App();
const stack = new Stack(app, "YourSite", {
env: { account: "your-account-id", region: "your-preferred-region" },
});
const { handler, distribution } = new SvelteKit(stack, "SvelteKit", {
runtime: "bun",
});By default the Lambda uses InvokeMode.RESPONSE_STREAM. To use buffered responses:
const { handler, distribution } = new SvelteKit(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.js
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
import adapter from "kit-on-lambda/bun";
const originUrl = "https://{distributionId}.cloudfront.net";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({ runtime: "node" }),
paths: {
assets: originUrl,
},
csrf: {
trustedOrigins: [originUrl],
},
},
};
export default config;// app.ts
import { SvelteKit } from "kit-on-lambda/cdk";
import { App, Stack, type Environment } from "aws-cdk-lib";
const app = new App();
const stack = new Stack(app, "YourSite", {
env: { account: "your-account-id", region: "your-preferred-region" },
});
const { handler, distribution } = new SvelteKit(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.ts
import { SvelteKit } from "kit-on-lambda/cdk";
import { App, Stack, type Environment } 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";
const app = new App();
const stack = new Stack(app, "YourSite", {
env: { account: "your-account-id", region: "your-preferred-region" },
});
const api = new HttpApi(stack, "Api");
const { handler, distribution } = new SvelteKit(stack, "SvelteKit", {
runtime: "node",
invokeMode: InvokeMode.BUFFERED,
toDefaultOrigin: ({ handler }) => {
const integration = new HttpLambdaIntegration("Integration", handler);
api.addRoutes({
path: "/{proxy+}",
methods: [HttpMethod.ANY],
integration,
});
api.addRoutes({
path: "/",
methods: [HttpMethod.ANY],
integration,
});
const apiUrl = new URL(api.apiEndpoint);
return new HttpOrigin(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";
const auth = new Auth(this, "Auth", {
alarms: props.alarms,
frontendUri: props.frontendUri,
stage: props.stage,
contributorInsights: false,
warmer: props.warmer,
allowSignUp: false,
});
const { distribution } = new SvelteKit(this, "SvelteKit", {
runtime: "node",
toDefaultOrigin: ({ handler }) => {
auth.addAuthorizedEndpoint({ lambda: handler });
auth.grantSdkAccess(handler);
return new HttpOrigin(Fn.parseDomainName(auth.api.url));
},
});
const authBehaviour = 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: