Skip to content

Latest commit

 

History

History
364 lines (278 loc) · 10.1 KB

File metadata and controls

364 lines (278 loc) · 10.1 KB

kit-on-lambda

npm version license

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

Installation

npm i kit-on-lambda aws-cdk aws-cdk-lib constructs
# or
bun i kit-on-lambda aws-cdk aws-cdk-lib constructs

To 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.

Architecture

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]
Loading

Option 1 — build with esbuild, run on Node.js runtime

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 deploy

By default the Lambda uses InvokeMode.RESPONSE_STREAM. To use buffered responses:

const { handler, distribution } = new SvelteKit(stack, "SvelteKit", {
  runtime: "node",
  invokeMode: InvokeMode.BUFFERED,
});

Accessing the AWS event and context (Node.js runtime)

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);
};

Option 2 — build with Bun, run on Bun runtime

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,
});

Option 3 — build with Bun, run on Node.js runtime

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",
});

Custom origins with toDefaultOrigin

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.

HTTP API Gateway origin

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);
  },
});

With a service that manages the authorizer

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.

Troubleshooting

SvelteKit named form actions — ?/actionName query parameter

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

Thank you

This package has been inspired by various other libraries. Some code has been adapted from: