Overview
The static-site pattern deploys a folder of built files — a Next.js export, a Vite build, an Astro site, hand-written HTML — to AWS with production hardening you would otherwise assemble by hand:- Private S3 bucket — no public bucket policy; CloudFront reaches it via Origin Access Control (OAC)
- CloudFront CDN — HTTPS, HTTP/2+3, global edge caching
- Clean URLs —
/aboutservesabout.htmlorabout/index.htmlto match your build output, or falls back toindex.htmlfor SPAs - Security headers — HSTS,
nosniff, frame denial, referrer policy - Custom domain — ACM certificate and Route53 alias record, created automatically
- Contact form — an optional
/api/contactendpoint (Lambda Function URL → SES) so a static site can still receive enquiries
The pattern serves files your build produces; it does not run a server. If
your site needs SSR or API routes beyond the contact form, use the
Payload pattern instead.
Quick Start
1. Have a build that emits static files
Any tool works, as long as one command produces a folder of files:2. Create the Fjall app
fjall create app with no flags and choose Static Site in the
interactive picker.
Paths are written into
fjall/my-site/infrastructure.ts and resolved from
that directory at deploy time — --source ../.. points at the repo root,
and --output-dir is relative to --source.3. Deploy
source, synthesises the CDN stack, and
uploads the contents of the output folder to S3. The deploy output includes
the CloudFront URL.
What’s Included
No Docker image is built and no ECR repository is created — the deploy
artefact is the static files themselves.
Routing Modes
Pick the mode that matches your build output: Astro’s default layout is
directory; Astro with build.format: "file" and Next.js output: "export"
produce multipage. fjall apps detect reads Astro’s build.format and
prefills the matching mode. Set with --routing <mode> at create time, or
routing: "<mode>" in infrastructure.ts.
Requests under /api/ and paths containing /. (such as
/.well-known/...) are never rewritten in any mode.
Deployment Process
First Deploy
- Detects
type: "staticsite"ininfrastructure.ts - Runs your build command in
source(10-minute budget) - Synthesises and deploys the single CDN stack
- Uploads the build output to S3 (stale files from previous builds are pruned)
- Prints the CloudFront (and custom-domain) URL
Subsequent Deploys
The same command rebuilds and re-uploads. Only changed files transfer, and pruning keeps the bucket in lockstep with the latest build — a deleted page disappears from S3 rather than lingering. After the upload the deployment invalidates the CloudFront distribution (/*), so the new content is served immediately instead of after the edge
cache’s TTL (up to 24 hours) expires. AWS grants 1,000 free invalidation
paths per month and /* counts as one path per deploy. Prefer pure TTL
semantics? Set cdn: { invalidateOnDeploy: false }.
Custom Domain
domain is set, the pattern needs to know which hosted zone the
domain lives in — zone identity is always explicit, never guessed from the
domain’s labels (label-guessing mis-resolved .com.au-style domains and
delegated sub-zones):
- Fjall-managed domain — nothing extra to configure. The CLI injects a
managed-domain binding at deploy time with literal values: the hosted zone
ID and the domain stack’s
us-east-1certificate ARN. CloudFront uses that certificate directly, so redeploying the site never churns a per-site certificate, and the binding works across accounts and regions. - Bring-your-own zone — set
zoneNameto the Route53 hosted zone that containsdomain(plushostedZoneIdto skip the runtime lookup). The pattern creates a DNS-validated ACM certificate inus-east-1and attaches it to CloudFront.
domain relative to the zone (example.com in zone example.com is the
apex; www.example.com is www).
Requires: a Route53 hosted zone containing the domain. See
Domain for creating, importing, or adopting
zones with fjall domain.
Contact Forms
A static site usually still needs one dynamic thing: a way for visitors to get in touch. Settingforms provisions a Lambda behind
https://<domain>/api/contact that emails submissions via SES.
The submitter’s own address goes in Reply-To, so replying from your inbox
reaches them directly.
Wiring a form
POST JSON or form-encoded data withname, email, and message fields.
Include an empty _gotcha field as a honeypot — bots that fill it get a
silent success and no email is sent.
Abuse posture
The endpoint is public and unauthenticated, so the pattern bounds what a caller can do rather than pretending to authenticate them:- Fixed sender and recipient — whatever a caller sends, the email can only
ever go from
noreply@<domain>to your configured address - Origin gate + CORS — browsers on other sites cannot POST to it; the
allow-origin defaults to
https://<domain>(override with--cors-origin) - Honeypot — filled
_gotchashort-circuits to a silent success - Reserved concurrency — the Lambda is capped (default 5 concurrent executions), so a flood cannot run up the SES bill or drain account-wide Lambda concurrency
- Scoped IAM — the Lambda may call
ses:SendEmailonly for the site’s domain identity, conditioned on the exactFromaddress
Security Headers
Strict-Transport-Security,
X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and
Referrer-Policy.
Content-Security-Policy is deliberately opt-in — a wrong policy silently
breaks inline scripts and hydration, so it is never a shipped default:
Pattern Parameters
Full Configuration Example
Root Options
Build Options
Security Options
Forms Options
CDN Options
priceClass is a latency knob, not a cost knob at small-site traffic:
PriceClass_100 serves from North America and Europe only — an audience in
Australia, Asia, or South America crosses an ocean on every request. Set
"PriceClass_All" for a global (or Oceania) audience; the price difference
at low traffic is effectively zero.
Access Gate Options
The gate is a CloudFront viewer function that challenges every request with
HTTP Basic auth before the routing rewrite runs. Use it to keep a
not-yet-launched site out of casual view and search indexes while a client
signs off — then remove it at launch.
Troubleshooting
Form POST returns 403
The Origin/Referer gate rejected the request. The browser must send the form from the configured origin — checkcorsOrigin matches the site origin
exactly (https://example.com, no path, no trailing slash), and that you are
not testing from localhost against the deployed endpoint.
Form POST returns 502
SES refused the send. Almost always identity verification: the domain is not verified as an SES identity in the deployment account/region, or the account is in the SES sandbox and theto address is unverified. Check the identity’s
status in the SES console.
Clean URLs return 403/404
Confirmrouting matches the build output: multipage for one .html file
per page, directory for a directory-with-index.html per page, spa for a
client-side router that needs every path to serve index.html.
Deploy fails: build output not found
outputDir is resolved relative to source. Run the build command manually
in source and check which folder it emits — Next.js exports to out, Vite
and Astro to dist.
Costs
Estimated monthly costs for a low-traffic site:Breaking Out the Pattern
The pattern is the opinionated layer — everything beneath it stays yours. Three escape tiers, in order of reach:1. Typed knobs
Most customisation needs a config field, not surgery:routing, security
(headers + CSP), forms, cdn.priceClass, cdn.invalidateOnDeploy,
cdn.behaviours (per-path CloudFront overrides), and accessGate are all
part of the pattern’s own surface, documented above.
2. Instance access — raw CDK underneath
app.addPattern returns the constructed pattern, and its getters reach every
resource it created:
3. Full decomposition — StorageFactory + CdnFactory
For total control, compose the resources yourself instead of using the pattern. The CDN Factory page’s Static Website with S3 example is the starting point: a private bucket viaStorageFactory, fronted by CdnFactory with OAC.
Next Steps
Payload Pattern
Need a CMS or SSR? Deploy Payload on Lambda
CDN Factory
Advanced CloudFront behaviours and origins
Storage Factory
S3 bucket configuration in depth
Deploy Application
General deployment guide
Related Documentation
Fjall:- Deploy Application - General deployment guide
- CDN Factory - CloudFront configuration
- Storage Factory - S3 options
- Next.js Static Exports -
output: "export"setup - Amazon SES identity verification - Verifying the sending domain