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
- Nuxt
- Astro
- TanStack Start
- SvelteKit
- Solid Start
- AnalogJS
- Any Nitro or Vite-based SSR framework via the generic
Serverlessconstruct
AWS Resources
| Resource | Purpose |
|---|---|
| Lambda Function | Runs SSR and API routes |
| S3 Bucket | Hosts static assets (JS, CSS, images) |
| CloudFront Distribution | Global CDN with origin routing |
| Origin Access Control | Secures S3 — no public bucket access |
| ACM Certificate | SSL/TLS for custom domain (optional) |
| Route53 | DNS management (optional) |
Architecture
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 pattern | Routed to | Notes |
|---|---|---|
*.* (any file extension) | S3 | JS, CSS, images, fonts — long-term cached |
/api/* (or custom paths) | Lambda | API routes, mutations |
Everything else /* | Lambda | SSR page rendering |
Quick Start
Installation
bun add -D @thunder-so/thunderConfiguration
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.
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
bun run buildnpx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultCDK outputs the CloudFront URL:
Outputs:myapp-web-prod-stack.CloudFrontUrl = https://d1234abcd.cloudfront.netCustom Domain
A certificate in us-east-1 is required for CloudFront:
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).
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:
const config: NuxtProps = { // ... serverProps: { paths: ['/api/*', '/trpc/*', '/auth/*'], },};Environment Variables and Secrets
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:
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).
const config: NuxtProps = { // ... serverProps: { dockerFile: 'Dockerfile', memorySize: 2048, },};FROM public.ecr.aws/lambda/nodejs:22
# Copy all lambda filesCOPY . ./
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:
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 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.
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
| Output | Description |
|---|---|
CloudFrontUrl | CloudFront distribution URL |
Route53Domain | Custom domain URL (only if domain is configured) |
Destroy
npx cdk destroy --app "bunx tsx stack/prod.ts" --profile default