--- 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 ```