AWS Deployment Shape (CloudFront + S3 + Lambda URL)
This document describes the recommended production topology for FaceTheory apps and the cache behavior split for SSR/SSG/ISR.
Reference Topology
- CloudFront distribution (single public entrypoint)
- Origin A: S3 for static client assets and prebuilt static pages
- Origin B: Lambda Function URL for SSR + ISR regeneration paths
- S3 bucket(s)
- immutable client assets (Vite output)
- optional SSG HTML output and strict-CSP hydration JSON sidecars under
/_facetheory/data/* - optional ISR HTML object storage plus pointer-paired strict-CSP hydration sidecars (via
S3HtmlStore; AWS SDK v3 client adapter:@theory-cloud/facetheory/aws-s3)
- Lambda-routed FaceTheory resource paths
- SSR runtime hydration sidecars under
/_facetheory/ssr-data/*, handled by the same FaceApp/Lambda path that rendered the SSR HTML
- SSR runtime hydration sidecars under
- DynamoDB table
- ISR metadata + lease/lock state (via TableTheory
FaceTheoryIsrMetaStoreand FaceTheory adapter@theory-cloud/facetheory/tabletheory)
- ISR metadata + lease/lock state (via TableTheory
AppTheory CDK (Recommended)
AppTheory CDK ships AppTheorySsrSite, which implements this topology and wires recommended environment variables onto
your SSR Lambda function.
When using AppTheorySsrSite in ssg-isr mode:
- use
staticPathPatternsfor cacheable extensionless HTML sections that should stay on S3 - use
directS3PathPatternsfor raw object/data paths such as/.vite/*and/_facetheory/data/* - use
ssrPathPatternsfor same-origin dynamic routes that must bypass the S3-primary origin group and go straight to Lambda, including the reserved SSR hydration sidecar prefix/_facetheory/ssr-data/* - prefer an
AWS_IAMFunction URL origin for read-only SSR traffic rather than a public direct URL - do not forward viewer-supplied tenant headers by default; derive tenancy from trusted request context when possible
- attach route-owned CSP headers from the Face response when strict no-inline CSP is required; CloudFront baseline
security headers do not replace the per-route
content-security-policyemitted bybuildStrictCspHeader()
Mutating form routes behind Lambda Function URL OAC are dynamic routes. If an SSR, SSG, ISR, or SPA page renders a
same-origin form that performs POST, PUT, PATCH, or DELETE, route the form action path through ssrPathPatterns
so CloudFront sends it to Lambda/AppTheory instead of S3 or an origin group static hit. Keep the Lambda Function URL on
AWS_IAM + OAC and install FaceTheory’s startAwsOacFormTransport() helper on forms marked with
data-facetheory-oac-form; native browser form posts cannot add the x-amz-content-sha256 payload hash header that
CloudFront signs for mutating Lambda URL requests.
Treat x-amz-content-sha256 as AWS signing plumbing only. It is not application authentication, authorization, CSRF
protection, or idempotency. Browser-generated multipart/form-data is intentionally out of scope for the URL-encoded
helper; marked multipart/text/plain forms fail closed until a separately scoped transport constructs and hashes the
exact body bytes itself. Setting ssrUrlAuthType: NONE is only an explicitly authorized, time-boxed rollback while a
broken deployment is repaired, and the rollback plan must restore AWS_IAM + OAC rather than making unauthenticated
Function URLs durable.
Reference example (FaceTheory repo):
infra/apptheory-ssr-site/infra/apptheory-ssg-isr-site/(SSG origin-group + ISR example)
When wireRuntimeEnv:true (default), the SSR function will receive:
APPTHEORY_ASSETS_BUCKETAPPTHEORY_ASSETS_PREFIXAPPTHEORY_ASSETS_MANIFEST_KEY(Vite commonly emits.vite/manifest.json; ensure your stack config matches)
If a cache table name is configured, AppTheory also wires these aliases:
APPTHEORY_CACHE_TABLE_NAMEFACETHEORY_CACHE_TABLE_NAMECACHE_TABLE_NAMECACHE_TABLE
FaceTheory ISR HTML storage is provided by S3HtmlStore and typically needs these env vars in the SSR runtime:
FACETHEORY_ISR_BUCKET(S3 bucket name)FACETHEORY_ISR_PREFIX(S3 prefix used by yourS3HtmlStoreinstance)
Note on prefixes:
S3HtmlStorehas akeyPrefix(physical S3 prefix).- FaceTheory ISR runtime has
htmlPointerPrefix(logical prefix embedded in stored pointers).
Avoid configuring both to the same non-empty prefix, or you’ll end up with prefix/prefix/... keys.
Build Outputs (Recommended Contract)
This is the deployment contract FaceTheory assumes for typical Vite SSR apps:
- SSR Lambda handler: exports a Lambda Function URL handler (streaming-capable).
- Preferred wiring: AppTheory
createLambdaFunctionURLStreamingHandler(app)+ FaceTheory AppTheory adapter (@theory-cloud/facetheory/apptheory).
- Preferred wiring: AppTheory
- Assets bucket layout: S3 keys should match the URL path that CloudFront routes to S3.
- Hashed build assets should live under an asset prefix (commonly
assets/), and be served with long-lived caching. - Vite manifest key should be explicitly configured in CDK to match your build output path
(commonly
.vite/manifest.json).
- Hashed build assets should live under an asset prefix (commonly
- Optional SSG hydration JSON:
- keys under
/_facetheory/data/*(SSG) routed to S3 - strict no-inline SSG routes with hydration data automatically externalize it to this path shape, for example
/about->/_facetheory/data/about.jsonand/->/_facetheory/data/index.json - upload and invalidate SSG HTML and its matching hydration JSON together; a stale HTML document should not point to a data sidecar from a different build
- keys under
- Optional SSR hydration JSON:
- signed framework-owned URLs under
/_facetheory/ssr-data/*when SSR Faces usecreateFaceApp({ ssrHydrationSidecars }) - route the reserved prefix to the same Lambda/FaceApp handler that rendered the SSR HTML
- do not upload SSR sidecars to S3 and do not reuse
/_facetheory/data/*for SSR runtime data
- signed framework-owned URLs under
See:
infra/apptheory-ssr-site/for SSR + assets via AppTheoryAppTheorySsrSiteinfra/apptheory-ssg-isr-site/for SSG origin-group failover + ISR (S3 + Dynamo)
Strict CSP Hydration Routing
Strict no-inline CSP changes hydration transport but not the AWS topology. The page still arrives through the same CloudFront distribution, and every bootstrap/data URL must resolve same-origin.
SSG sidecars:
buildSsgSite()writes strict hydration JSON under/_facetheory/data/*when an SSG Face hascsp.inlineScripts === falseand hydration data.- Route
/_facetheory/data/*to S3 with the same origin access posture as the static HTML output. - Serve sidecars as
application/json; charset=utf-8with explicit cache headers. Their TTL should be no looser than the HTML that references them unless you deploy sidecars under immutable build-specific prefixes. - Deploy and invalidate HTML and data sidecars as a pair so client hydration reads the same data used for the server render.
ISR sidecars:
- Strict ISR regeneration stores a hydration sidecar next to the generated HTML pointer in
S3HtmlStore. - The sidecar pointer is derived from the HTML pointer by replacing
.htmlwith.hydration.json, so stale HTML and stale hydration data remain paired through the same metadata pointer lifecycle. - The browser-facing data URL stays on the page route and uses the opaque
__facetheory_isr_hydrationquery parameter. That request must route to Lambda/FaceTheory, not directly to S3, so the runtime can resolve the tenant/cache-key variant, validate the pointer binding, and serve the sidecar only for an equivalent request context. - Do not create public CloudFront behaviors that expose arbitrary ISR sidecar object keys. The runtime validates the
pointer token and request-variant binding, then returns
404for invalid, missing, or cross-variant sidecars. The sidecar URL is not a bearer credential; tenant, authorization-like headers, cookies, and query variants must still match the HTML generation boundary.
SSR sidecars:
- Per-request strict SSR data sidecars are framework-owned when the app is configured with
createFaceApp({ ssrHydrationSidecars }). - FaceTheory emits signed same-origin URLs under
/_facetheory/ssr-data/*. - Route
/_facetheory/ssr-data/*to the same Lambda/FaceApp handler that rendered the SSR HTML so the runtime can return the exact hydration payload produced during that render without rerunning the Face. - Keep this prefix distinct from
/_facetheory/data/*; the latter is the SSG/static sidecar namespace and should remain S3-routed.
OAC and navigation:
startAwsOacFormTransport({ navigationPolicy: "full-page" })is the strict-CSP default because fetched CSP headers cannot become the active document policy duringdocument.write()replacement.- Choose
navigationPolicy: "spa"only when the returned HTML is a FaceTheory document, the host accepts the SPA navigation contract, external hydration data is loaded before DOM mutation, and the fetched HTML does not carryContent-Security-Policy; use full-page navigation or caller-owned handling for CSP-protected HTML. - Route mutating action paths through
ssrPathPatterns; do not let S3 static behaviors intercept form actions from SSG or ISR pages.
CloudFront Behaviors
There are two viable strategies for “SSG hits avoid Lambda”.
Strategy A (Recommended for large SSG route sets): origin group (S3 primary, Lambda URL failover)
Behaviors:
/assets/*+/.vite/*-> S3 origin/_facetheory/data/*-> S3 origin (if SSG hydration JSON files are emitted)/_facetheory/ssr-data/*-> Lambda URL origin (SSR runtime hydration sidecars)- Default
/*-> Origin group- primary: S3 origin (SSG HTML keys)
- failover: Lambda URL origin (SSR + ISR) on 403/404 misses
This requires a viewer-request CloudFront Function that:
- rewrites extensionless routes to the S3 HTML key shape (commonly
.../index.html) - copies the original viewer URI into a header (example:
x-facetheory-original-uri) so SSR can route correctly on failover
Pros:
- scales to large SSG route sets (no per-route behaviors)
Cons:
- static + dynamic share the same cache policy and origin request policy at the default behavior; your SSR handler must be strict about cache headers (e.g.
cache-control: private, no-store) - requires explicit URI rewrite discipline
Reference stack:
infra/apptheory-ssg-isr-site/
Strategy B: explicit behaviors for known SSG routes (small route sets)
Behaviors (top to bottom):
/assets/*+/.vite/*-> S3 origin/_facetheory/data/*-> S3 origin/_facetheory/ssr-data/*-> Lambda URL origin- Known static routes (SSG-first pages) -> S3 origin
- Default
/*-> Lambda URL origin
Notes:
- Keep static and dynamic origins separate so static hits do not traverse Lambda.
- Use Origin Access Control for S3.
- Use an origin request policy for Lambda that forwards only headers/cookies/query needed by app logic.
- Avoid modeling viewer-supplied tenant headers as part of the default origin request contract.
Cache and Header Policy by Rendering Mode
SSR (dynamic, request-dependent)
- Default response header recommendation:
cache-control: private, no-store
- Strict-CSP hydration sidecars under
/_facetheory/ssr-data/*should reach Lambda and return no-store JSON from the same FaceApp runtime that rendered the HTML. - If SSR output is anonymous and intentionally cacheable, use a bounded shared TTL (for example):
cache-control: public, max-age=0, s-maxage=60, stale-while-revalidate=30, stale-if-error=300
- If response includes
set-cookie, do not cache at CloudFront.
SSG (build-time static)
- HTML pages:
cache-control: public, max-age=0, s-maxage=600(or environment-specific value)
- Strict-CSP hydration JSON:
- serve from S3 under
/_facetheory/data/*with cache headers coordinated with the referencing HTML
- serve from S3 under
- Hashed assets (
/assets/*):cache-control: public, max-age=31536000, immutable
- Deploy updates should upload new hashed assets and invalidate changed HTML keys.
ISR (blocking)
- FaceTheory runtime default helper (
blockingIsrCacheControl) emits:cache-control: public, max-age=0, s-maxage=0, stale-if-error=<revalidateSeconds>, must-revalidate
- Runtime ISR state marker header:
x-facetheory-isr: miss | hit | wait-hit | stale
- CloudFront should not treat ISR HTML as long-lived immutable content; freshness is runtime-controlled via metadata.
ISR Request Flow
- Request reaches Lambda URL behavior.
- FaceTheory verifies tenant partition safety. Requests carrying known tenant boundary headers (
x-tenant-idorx-facetheory-tenant) fail closed unlesstenantKeyor a customcacheKeyis configured. - FaceTheory computes cache key (
tenant + route + params + query + authorization-like header/cookie digestsby default). - Metadata lookup in DynamoDB:
- fresh -> serve cached HTML pointer from S3
- stale/miss -> attempt lease lock and regenerate
- Strict-CSP regeneration writes the pointer-derived hydration sidecar before the HTML metadata commit.
- Regeneration writes HTML to S3, then atomically updates metadata pointer.
- On regeneration failure, previous pointer stays valid; stale HTML and stale hydration data stay paired.
- Hydration sidecar reads resolve the same tenant/cache-key request variant before serving JSON.
Default tenant note:
- FaceTheory uses the
defaulttenant unlesstenantKeyis configured. - If tenant identity comes from auth/session/host mapping or a trusted header, provide an explicit
tenantKeyor customcacheKey, or keep that route on SSR. - Tenant-invariant ISR routes should not receive tenant-like headers; if they do, FaceTheory fails closed rather than sharing cached HTML through the implicit
defaulttenant.
Operational Checklist
- Enable CloudFront access logs and Lambda logs for request correlation.
- Confirm Lambda timeout is safely above worst-case streaming/SSR duration.
- Enforce viewer protocol policy (redirect HTTP to HTTPS).
- Configure WAF/rate-limiting at CloudFront as needed.
- Use separate S3 prefixes (or buckets) for:
- immutable build assets
- ISR objects
- optional SSG output
- For strict-CSP routes, confirm:
- every strict response carries the intended
content-security-policyheader - SSG sidecars under
/_facetheory/data/*are reachable from CloudFront and same-origin - SSR sidecars under
/_facetheory/ssr-data/*route to Lambda/FaceTheory and return the render-produced JSON payload - ISR hydration sidecar query URLs route to Lambda/FaceTheory and do not expose raw object keys
- OAC form navigation policy is explicit when CSP-protected HTML responses are expected
- every strict response carries the intended
- Runbook should include:
- cache invalidation steps for SSG HTML changes
- rollback procedure for Lambda + metadata table changes