---
title: Static
description: Deploy SPAs and static sites using AWS S3 and CloudFront
---
import { Aside } from '@astrojs/starlight/components';
import StaticArchitecture from '@/components/docs/patterns/StaticArchitecture.astro';
import StaticPipeline from '@/components/docs/patterns/StaticPipeline.astro';
Deploy any client-side Single Page Application (SPA) or static site generator (SSG) on [AWS S3](https://aws.amazon.com/s3/) and [CloudFront](https://aws.amazon.com/cloudfront/). Thunder's `Static` construct ships with HTTPS, HTTP/3, Brotli compression, and security headers out of the box.
## Supported Frameworks
- [Astro (SSG mode)](https://astro.build/)
- [Next.js (static export)](https://nextjs.org/)
- [Vite](https://vite.dev/) — React, Vue, Svelte, Preact, Solid, Lit
- [VitePress](https://vitepress.dev/)
- [Gatsby (static)](https://www.gatsbyjs.com/)
- [React Router (client-side / SSG)](https://reactrouter.com/start/framework/rendering)
- Any framework that produces a static output folder
## AWS Resources
| Resource | Purpose |
| --- | --- |
| [S3 Bucket](https://aws.amazon.com/s3/) | Stores your build output. Private, accessed only via CloudFront OAC. |
| [CloudFront Distribution](https://aws.amazon.com/cloudfront/) | Global CDN. HTTP/3, TLS 1.2+, Brotli/Gzip compression. |
| [Origin Access Control](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) | Secures S3 — no public bucket access. |
| [ACM Certificate](https://aws.amazon.com/certificate-manager/) | SSL/TLS for custom domain (optional). |
| [Route53](https://aws.amazon.com/route53/) | DNS A + AAAA records for IPv4/IPv6 (optional). |
## Hosting Architecture
## Quick Start
### Installation
```bash
bun add -D @thunder-so/thunder
```
### Configuration
```ts title="stack/prod.ts"
import { Cdk, Static, type StaticProps } from "@thunder-so/thunder";
const config: StaticProps = {
env: {
account: 'YOUR_ACCOUNT_ID',
region: 'us-east-1'
},
application: 'myapp',
service: 'web',
environment: 'prod',
rootDir: '.', // monorepo: e.g. 'apps/web'
outputDir: 'dist',
};
new Static(
new Cdk.App(),
`${config.application}-${config.service}-${config.environment}-stack`,
config
);
```
### Deploy
```bash
bun run build
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default
```
CDK outputs the CloudFront URL:
```
Outputs:
myapp-web-prod-stack.DistributionUrl = https://d1234abcd.cloudfront.net
```
## Custom Domain
Connect your own domain using [Route53](https://aws.amazon.com/route53/) and an [ACM certificate](https://aws.amazon.com/certificate-manager/). The certificate must be issued in `us-east-1` — CloudFront is a global service and requires it there regardless of your app's region.
```ts title="stack/prod.ts"
const config: StaticProps = {
// ...
domain: 'app.example.com',
hostedZoneId: 'Z1D633PJN98FT9',
globalCertificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/abc-123',
};
```
## CloudFront Cache Behavior
Control what gets included in the cache key and forwarded to the origin:
```ts title="stack/prod.ts"
const config: StaticProps = {
// ...
errorPagePath: '/404.html', // custom 404 page (default: /index.html)
allowHeaders: ['Accept-Language'],
allowCookies: ['session-*'],
allowQueryParams: ['lang', 'theme'],
// denyQueryParams: ['utm_source', 'fbclid'], // mutually exclusive with allowQueryParams
};
```
## Lambda@Edge
### URL Redirects and Rewrites
Configure URL redirects and rewrites using [Lambda@Edge](https://aws.amazon.com/lambda/edge/) functions that run at CloudFront edge locations worldwide. Redirects return HTTP 301 responses to the client; rewrites transparently serve different content without changing the URL.
```ts title="stack/prod.ts"
const config: StaticProps = {
// ...
redirects: [
{ source: '/home', destination: '/' },
{ source: '/blog/:slug', destination: '/posts/:slug' },
],
rewrites: [
{ source: '/app/*', destination: '/index.html' }, // SPA fallback
],
};
```
**Pattern syntax:**
| Pattern | Matches |
| --- | --- |
| `/about` | Exact path `/about` |
| `/blog/*` | Any path starting with `/blog/` |
| `/user/:id` | `/user/123`, `/user/abc`, etc. |
| `/a/:x/b/:y` | `/a/foo/b/bar` → `x=foo`, `y=bar` |
### Custom Headers
Add custom HTTP response headers per path pattern. Headers run at the `viewer-response` stage and apply to both cached and uncached responses.
```ts title="stack/prod.ts"
const config: StaticProps = {
// ...
headers: [
{ path: '/assets/*', name: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
{ path: '/**', name: 'X-Frame-Options', value: 'SAMEORIGIN' },
],
};
```
**Path syntax:**
| Path | Matches |
| --- | --- |
| `/*` | Root-level paths only |
| `/**` | All paths including nested |
| `/blog/*` | All paths under `/blog/` |
## CI/CD Pipeline
### AWS CodePipeline Integration
### Configuration
```ts title="stack/prod.ts"
const config: StaticProps = {
// ...
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 |
| --- | --- |
| `DistributionId` | CloudFront distribution ID |
| `DistributionUrl` | CloudFront URL (`https://xxxx.cloudfront.net`) |
| `Route53Domain` | Custom domain URL (only if `domain` is configured) |
## Destroy
```bash
npx cdk destroy --app "bunx tsx stack/prod.ts" --profile default
```