Deploy your TanStack Start app with server-side rendering to AWS using Thunder's TanStackStart construct. This creates a hybrid architecture: Lambda handles SSR and API routes, S3 hosts static assets, and CloudFront unifies both.
bunx create-tanstack-start my-app
cd my-appReference: TanStack Start Quick Start
TanStack Start uses Nitro for server-side rendering. You must explicitly set the aws-lambda preset - the default is node-server which won't work on Lambda.
Edit app.config.ts (or create it if it doesn't exist):
import { defineConfig } from "@tanstack/start/config";
import { nitro } from "nitro/vite";
export default defineConfig({
vite: {
plugins: [
nitro({
preset: "aws-lambda", // Required for Lambda deployment
}),
],
},
});Alternative: If using @tanstack/nitro-v2-vite-plugin:
import { nitroV2Plugin } from "@tanstack/nitro-v2-vite-plugin";
export default defineConfig({
vite: {
plugins: [
nitroV2Plugin({
preset: "aws-lambda",
}),
],
},
});Reference: Nitro AWS Lambda Preset
bun run buildThis generates:
.output/server/- Lambda handler.output/public/- Static assets for S3
bun add @thunder-so/thunder --developmentCreate stack/prod.ts:
import {
Cdk,
TanStackStart,
type TanStackStartProps,
} from "@thunder-so/thunder";
const config: TanStackStartProps = {
env: {
account: "123456789012", // Your AWS account ID
region: "us-east-1",
},
application: "myapp",
service: "web",
environment: "prod",
rootDir: ".",
};
new TanStackStart(
new Cdk.App(),
`${config.application}-${config.service}-${config.environment}-stack`,
config,
);npx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultCDK outputs the CloudFront and API Gateway URLs:
Outputs:
myapp-web-prod-stack.CloudFrontUrl = https://d1234abcd.cloudfront.net
myapp-web-prod-stack.ApiGatewayUrl = https://abc123.execute-api.us-east-1.amazonaws.com
- Create a Route53 Hosted Zone
- Request a global ACM certificate in
us-east-1(for CloudFront) - Request a regional ACM certificate in your function's region (for API Gateway)
Update your stack:
const config: TanStackStartProps = {
// ...
domain: "app.example.com",
hostedZoneId: "Z1234567890ABC",
globalCertificateArn:
"arn:aws:acm:us-east-1:123456789012:certificate/abc-123",
regionalCertificateArn:
"arn:aws:acm:us-east-1:123456789012:certificate/def-456",
};const config: TanStackStartProps = {
// ...
serverProps: {
variables: [
{ NODE_ENV: "production" },
{ API_BASE_URL: "https://api.example.com" },
],
},
};aws secretsmanager create-secret \
--name "/myapp/DATABASE_URL" \
--secret-string "postgres://user:pass@host/db"serverProps: {
secrets: [
{ key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123' },
],
},serverProps: {
memorySize: 1792, // More memory = more CPU
timeout: 10,
keepWarm: true, // Ping every 5 min to prevent cold starts
tracing: true, // Enable AWS X-Ray
},For larger apps, use Docker:
Create Dockerfile:
FROM public.ecr.aws/lambda/nodejs:22
COPY .output/server/ ./
CMD ["index.handler"]Update your stack:
serverProps: {
dockerFile: 'Dockerfile',
memorySize: 2048,
},Error: Runtime.HandlerNotFound: index.handler is undefined
You forgot to set preset: 'aws-lambda' in your Nitro config. The default node-server preset outputs an HTTP server, not a Lambda handler.
Static assets not loading
Check that your build output is in .output/public/. Thunder automatically deploys this to S3 and routes *.* requests through CloudFront.
- serverless.md - Serverless construct overview
- lambda-basic.md - Lambda construct reference
- static-basic.md - Static construct reference