Serverless Fullstack

Deploy full-stack server-side rendered applications on AWS using Lambda for SSR, S3 for static assets, and CloudFront as the global CDN. Scales to zero — pay only for actual requests.

Supported Frameworks

AWS Resources

ResourcePurpose
Lambda FunctionRuns SSR and API routes
S3 BucketHosts static assets (JS, CSS, images)
CloudFront DistributionGlobal CDN with origin routing
Origin Access ControlSecures S3 — no public bucket access
ACM CertificateSSL/TLS for custom domain (optional)
Route53DNS management (optional)

Architecture

Users
CloudFront
Lambda (SSR)
S3 (Assets)

CloudFront routes requests at the edge — static assets are served directly from S3 while dynamic SSR requests are forwarded to Lambda via API Gateway, which renders pages on-demand with pay-per-request pricing and automatic scaling.

CloudFront routes requests at the edge using this logic:

Request patternRouted toNotes
*.* (any file extension)S3JS, CSS, images, fonts — long-term cached
/api/* (or custom paths)LambdaAPI routes, mutations
Everything else /*LambdaSSR page rendering

Quick Start

Installation

Terminal window
bun add -D @thunder-so/thunder

Configuration

Use the framework-specific construct for best defaults. Each construct knows the expected build output paths for its framework and configures Lambda, S3, and CloudFront accordingly.

stack/prod.ts
import { Cdk, Nuxt, type NuxtProps } from '@thunder-so/thunder';
const config: NuxtProps = {
env: {
account: 'YOUR_ACCOUNT_ID',
region: 'us-east-1',
},
application: 'myapp',
service: 'web',
environment: 'prod',
rootDir: '.',
serverProps: {
runtime: Cdk.aws_lambda.Runtime.NODEJS_22_X,
architecture: Cdk.aws_lambda.Architecture.ARM_64,
memorySize: 1792,
timeout: 10,
keepWarm: true,
},
};
new Nuxt(
new Cdk.App(),
`${config.application}-${config.service}-${config.environment}-stack`,
config
);

Deploy

Terminal window
bun run build
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default

CDK outputs the CloudFront URL:

Outputs:
myapp-web-prod-stack.CloudFrontUrl = https://d1234abcd.cloudfront.net

Custom Domain

A certificate in us-east-1 is required for CloudFront:

stack/prod.ts
const config: NuxtProps = {
// ...
domain: 'app.example.com',
hostedZoneId: 'Z1D633PJN98FT9',
certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abc-123',
};

Advanced Configuration

Server Runtime

Fine-tune Lambda performance with memory, timeout, concurrency, and warm-up settings. streaming enables response streaming for frameworks that support it (Nuxt/Nitro).

stack/prod.ts
const config: NuxtProps = {
// ...
serverProps: {
runtime: Cdk.aws_lambda.Runtime.NODEJS_22_X,
architecture: Cdk.aws_lambda.Architecture.ARM_64,
memorySize: 1792,
timeout: 10,
tracing: true, // enable AWS X-Ray
keepWarm: true, // ping every 5 min to prevent cold starts
streaming: true, // enable response streaming (Nitro)
reservedConcurrency: 10, // hard cap on simultaneous executions
provisionedConcurrency: 2, // pre-warmed instances, eliminates cold starts
},
};

Custom API Paths

By default, CloudFront routes /api/* to Lambda and everything else either to S3 (static files) or Lambda (SSR). Use paths to customize which URL patterns are treated as API routes:

stack/prod.ts
const config: NuxtProps = {
// ...
serverProps: {
paths: ['/api/*', '/trpc/*', '/auth/*'],
},
};

Environment Variables and Secrets

stack/prod.ts
const config: NuxtProps = {
// ...
serverProps: {
variables: [
{ NODE_ENV: 'production' },
],
secrets: [
{ key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123' },
],
},
};

CloudFront Cache Behavior

Control what gets included in the CloudFront cache key for SSR responses:

stack/prod.ts
const config: NuxtProps = {
// ...
allowHeaders: ['Accept-Language'],
allowCookies: ['session-*'],
allowQueryParams: ['lang', 'theme'],
// denyQueryParams: ['utm_source', 'fbclid'], // mutually exclusive with allowQueryParams
};

Container Mode

Zip deployments have a 250 MB unzipped size limit. For apps with large dependencies, switch to container mode. Thunder builds a Docker image, pushes it to ECR, and deploys it as a container Lambda (up to 10 GB).

stack/prod.ts
const config: NuxtProps = {
// ...
serverProps: {
dockerFile: 'Dockerfile',
memorySize: 2048,
},
};
Dockerfile
FROM public.ecr.aws/lambda/nodejs:22
# Copy all lambda files
COPY . ./
CMD ["index.handler"]

Generic Serverless Construct

For any Vite/Nitro-based framework not explicitly supported, use the generic Serverless construct and specify the server output paths manually:

stack/prod.ts
import { Cdk, Serverless, type ServerlessProps } from '@thunder-so/thunder';
const config: ServerlessProps = {
env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' },
application: 'myapp',
service: 'web',
environment: 'prod',
rootDir: '.',
serverProps: {
codeDir: '.output/server',
handler: 'index.handler',
runtime: Cdk.aws_lambda.Runtime.NODEJS_22_X,
architecture: Cdk.aws_lambda.Architecture.ARM_64,
memorySize: 1792,
timeout: 10,
},
clientProps: {
outputDir: '.output/public',
},
};
new Serverless(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config);

CI/CD Pipeline

AWS CodePipeline Integration

GitHub
CodePipeline
CodeBuild
Lambda (SSR)
CloudFront
S3 (Assets)

GitHub triggers the pipeline on code changes. CodeBuild builds the framework and packages the SSR server for Lambda while uploading static assets to S3, then invalidates the CloudFront global cache to serve the latest version.

stack/prod.ts
const config: NuxtProps = {
// ...
accessTokenSecretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:github-token-XXXXXX',
sourceProps: {
owner: 'your-username',
repo: 'your-repo',
branchOrRef: 'main',
},
buildProps: {
runtime: 'nodejs',
runtime_version: '22',
installcmd: 'bun install',
buildcmd: 'bun run build',
},
};

Stack Outputs

OutputDescription
CloudFrontUrlCloudFront distribution URL
Route53DomainCustom domain URL (only if domain is configured)

Destroy

Terminal window
npx cdk destroy --app "bunx tsx stack/prod.ts" --profile default