FaceTheory-First SSR Site (CloudFront + S3 + Lambda URL)
Use this guide when you want the canonical AppTheory deployment pattern for FaceTheory-style SSR, SSG, and ISR on AWS.
AppTheorySsrSite is the supported AppTheory companion construct for this shape. The example under
examples/cdk/ssr-site/ is the canonical implementation to copy from; it is not a weaker helper path separate from
the FaceTheory deployment contract.
For v1.0 fail-closed migration notes covering Function URL auth defaults, tenant-header trust, and related cache-key
changes, see docs/migration/v1-security.md.
Preferred mode
Prefer mode: AppTheorySsrSiteMode.SSG_ISR unless you are intentionally keeping a narrower compatibility path.
ssg-isris the FaceTheory-first topology:/assets/*and/_facetheory/data/*stay on direct S3 behaviors/_facetheory/ssr-data/*routes directly to the SSR Lambda Function URL for strict no-inline-CSP SSR sidecars- default
/*uses a primary HTML S3 origin with Lambda Function URL fallback forGET/HEAD/OPTIONS - extensionless HTML routes rewrite to
/index.htmlat the edge, except for direct S3/data and direct SSR carve-outs - same-origin dynamic paths such as actions, auth callbacks, and form posts should be carved out with
ssrPathPatternsso they bypass the origin group and route straight to Lambda
ssr-onlyremains available for compatibility, but it is not the preferred documented deployment story for new FaceTheory work.
Canonical stack shape
import { AppTheorySsrSite, AppTheorySsrSiteMode } from "@theory-cloud/apptheory-cdk";
new AppTheorySsrSite(this, "Site", {
ssrFunction,
mode: AppTheorySsrSiteMode.SSG_ISR,
assetsBucket,
assetsKeyPrefix: "assets",
assetsManifestKey: ".vite/manifest.json",
// FaceTheory ISR HTML store (`S3HtmlStore`)
htmlStoreBucket: isrBucket,
htmlStoreKeyPrefix: "isr",
// FaceTheory ISR metadata + lease coordination (TableTheory schema)
isrMetadataTable,
// Cacheable HTML sections that should stay on S3.
staticPathPatterns: ["/marketing/*"],
// Dynamic same-origin routes that should stay on the SSR Lambda.
// AppTheory adds /_facetheory/ssr-data/* automatically in ssg-isr mode.
ssrPathPatterns: ["/actions/*"],
// Bearer-auth API co-origins that share the distribution without weakening
// the SSR origin's AWS_IAM + Lambda OAC posture.
bearerFunctionUrlOrigins: [
{
function: controlPlaneApiFunction,
pathPatterns: ["/api/*", "/auth/*", "/setup/*"],
},
{
function: trustApiFunction,
pathPatterns: ["/.well-known/*", "/attestations/*"],
},
],
// Optional explicit compatibility override. Omit this to keep the default
// CloudFront-signed AWS_IAM Function URL origin.
// ssrUrlAuthType: lambda.FunctionUrlAuthType.NONE,
// Optional app-specific origin headers
ssrForwardHeaders: ["x-facetheory-segment"],
});
Custom domains and CloudFront certificates
CloudFront viewer certificates must be in us-east-1. For environment-agnostic stacks and stacks outside
us-east-1, provide an explicit certificateArn that points at a us-east-1 ACM certificate.
When domainName and hostedZone are set without certificateArn, AppTheorySsrSite uses non-deprecated
acm.Certificate semantics and allows hosted-zone auto-certificate creation only if the stack region is explicitly
us-east-1. Other regions and unresolved regions fail closed during synthesis instead of creating an unsafe same-region
certificate.
FaceTheory data sidecars
ssg-isr mode reserves two FaceTheory data prefixes with different origins:
/_facetheory/data/*and exact/_facetheory/dataare SSG sidecars and stay on direct S3 behaviors./_facetheory/ssr-data/*and exact/_facetheory/ssr-dataare SSR sidecars and route directly to the SSR Lambda Function URL.
The SSR sidecar behavior is automatic; do not add it to directS3PathPatterns. AppTheory includes it in the same
fail-closed path ownership checks as assets, static HTML sections, direct SSR paths, and bearer Function URL co-origins.
Exact, broader, or nested wildcard patterns that would shadow the reserved SSR sidecar prefix are rejected, including
CloudFront * and ? path-pattern wildcards. The sidecar behavior also bypasses the ssg-isr HTML rewrite so
extensionless sidecar URLs cannot become /index.html requests before origin selection. The behavior uses the same SSR
Lambda origin request policy and preserves the default AWS_IAM plus Lambda Origin Access Control posture when
ssrUrlAuthType is omitted.
Contract
AppTheorySsrSite now assumes the stronger FaceTheory deployment contract by default:
- SSR origin:
- omitted
ssrUrlAuthTypenow fails closed toAWS_IAM+ lambda Origin Access Control for all CloudFront-to-Lambda traffic - set
ssrUrlAuthType: lambda.FunctionUrlAuthType.NONEonly when you intentionally require public direct Function URL access as a compatibility choice
- omitted
- Edge request normalization:
- viewer-request preserves an inbound
x-request-id, otherwise falls back to the CloudFront request ID - viewer-request records both
x-apptheory-original-host/x-apptheory-original-uriandx-facetheory-original-host/x-facetheory-original-uri - viewer-request strips
x-tenant-idby default, and tenant-likessrForwardHeadersare rejected unless you explicitly enable compatibility passthrough - raw
hostandx-forwarded-protoare intentionally rejected from the SSR origin request allowlist
- viewer-request preserves an inbound
- CDN response headers:
- baseline security headers are set at CloudFront: HSTS,
nosniff, frame options, referrer policy, XSS protection, and restrictivepermissions-policy - Content-Security-Policy stays origin-defined so per-request SSR nonce flows remain possible
- baseline security headers are set at CloudFront: HSTS,
- Cache semantics:
ssr-onlydefaults Lambda-backed behaviors toCACHING_DISABLED; opt intossrCachePolicyonly when the Lambda response variance model is explicitly cache-safessg-isruses a dedicated public HTML cache policy on the default behavior and anystaticPathPatternsHTML behaviors:- all query strings are part of the cache key
- cookies are excluded from the cache key and are not forwarded to the HTML S3 origin
- stable public variant headers (
x-*-original-hostand non-tenant opted-in forwarded headers) are part of the cache key by default - tenant-like viewer headers join the cache key only when
allowViewerTenantHeaders: trueis explicitly enabled - origin cache-control headers still drive freshness within that safe cache key
- direct S3 asset/data behaviors use the AppTheory static asset cache policy: no viewer headers, cookies, or query
strings are forwarded to the S3/OAC origin (including the viewer
Hostheader), origin cache-control headers drive freshness with a 0-second minimum TTL, and objects without origin cache-control fall back to a 1-day default TTL - the reserved SSR sidecar behavior (
/_facetheory/ssr-data/*) is Lambda-backed and defaults toCACHING_DISABLED
Mixed-auth Lambda Function URL co-origins
Use bearerFunctionUrlOrigins when the same CloudFront distribution must host AppTheory-managed FaceTheory SSR plus
additional Lambda Function URL APIs that authenticate inside handler code. This is the supported AppTheory path for
mixed-auth co-origins. Do not hand-wire raw site.distribution.addBehavior(...) calls when AppTheory should own path
collision checks, SSG/ISR rewrite bypasses, and edge request-id/original-host policy.
new AppTheorySsrSite(this, "Site", {
ssrFunction,
mode: AppTheorySsrSiteMode.SSG_ISR,
// Omit ssrUrlAuthType so the SSR origin keeps AWS_IAM + Lambda OAC.
bearerFunctionUrlOrigins: [
{
function: controlPlaneApiFunction,
pathPatterns: ["/api/*", "/auth/*", "/setup/*"],
},
{
function: trustApiFunction,
pathPatterns: ["/.well-known/*", "/attestations/*"],
},
],
});
AppTheory creates each co-origin Function URL with lambda.FunctionUrlAuthType.NONE. The co-origin behaviors use
AllowedMethods.ALLOW_ALL, CachePolicy.CACHING_DISABLED, and
OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, so bearer headers reach the API without forwarding the viewer
Host header to Lambda Function URLs. The site’s response headers policy and viewer request/response functions are
applied to these behaviors, preserving x-request-id echo and x-apptheory-* / x-facetheory-* original host and URI
headers.
The SSR Lambda OAC posture is origin-scoped: AppTheory attaches Lambda OAC only to the SSR Function URL origin when
ssrUrlAuthType is omitted or set to AWS_IAM. Bearer co-origins remain non-OAC AuthType.NONE Function URLs and are
expected to enforce authentication in handler code.
Tenant trust
AppTheorySsrSite now distinguishes forwarded edge context from trusted tenant derivation:
x-apptheory-original-host/x-facetheory-original-hostremain the safe edge context headers for host-aware SSR.- viewer-supplied tenant headers are not trusted by default:
x-tenant-idis stripped before the request reaches the origin- tenant-like entries in
ssrForwardHeadersare rejected unless compatibility passthrough is explicitly enabled
- if your tenancy depends on host mapping, derive the tenant inside your SSR function from the original-host headers and your allowlisted domain mapping
Compatibility escape hatch:
new AppTheorySsrSite(this, "Site", {
ssrFunction,
allowViewerTenantHeaders: true,
ssrForwardHeaders: ["x-facetheory-tenant"],
});
Use allowViewerTenantHeaders: true only as a migration bridge for existing FaceTheory-first deployments that still
depend on viewer-supplied tenant headers. It restores legacy passthrough for x-tenant-id and tenant-like
ssrForwardHeaders, but those headers remain viewer-controlled, not trusted edge-derived values.
Runtime env wiring
When wireRuntimeEnv is left enabled (the default), AppTheory wires:
APPTHEORY_ASSETS_BUCKETAPPTHEORY_ASSETS_PREFIXAPPTHEORY_ASSETS_MANIFEST_KEYFACETHEORY_ISR_BUCKETFACETHEORY_ISR_PREFIXAPPTHEORY_CACHE_TABLE_NAMEFACETHEORY_CACHE_TABLE_NAMECACHE_TABLE_NAMECACHE_TABLE
If you pass htmlStoreBucket and isrMetadataTable, AppTheory also grants the SSR Lambda the required S3 and
DynamoDB permissions. If you use name-only wiring for the metadata table, you still need to grant access in your app
stack.
SSR_ONLY provided-assets validation example
examples/cdk/ssr-only-provided-assets-site/ is a narrow validation example for operators who need to prove
ssr-only asset delivery with a caller-provided, stack-owned assets bucket. It is not a second architecture and it does
not change the preferred FaceTheory-first ssg-isr guidance above.
The example deliberately omits assetsPath on AppTheorySsrSite; it passes assetsBucket and uploads the known JS,
CSS, and text probe objects with an example-local BucketDeployment. The SSR Lambda returns HTML that references
/assets/app.js and /assets/site.css, matching Vite/FaceTheory-style absolute asset URLs while avoiding a
FaceTheory dependency for this smoke target.
Security posture stays on the AppTheory default path:
- the SSR Lambda Function URL remains
AWS_IAMand is reached through CloudFront Lambda OAC - the provided assets bucket blocks public access, enforces SSL, and receives only the AppTheory-generated CloudFront
service-principal read grant scoped to the distribution
SourceArn /assets/*and exact/assetsstay on AppTheory-managed direct S3 OAC behaviors with the standard viewer-request/viewer-response functions for request-id propagation and the no-viewer-Hoststatic asset cache policy- no legacy OAI comparison path is part of the example
Local deterministic proof:
./scripts/verify-ssr-only-provided-assets-synth.sh
Authorized live handoff, when Factory explicitly approves AWS mutation:
AWS_PROFILE=Mcp ./scripts/verify-ssr-only-provided-assets-site-smoke.sh
Set KEEP_STACK=1 only for an explicitly authorized debugging run; otherwise the smoke helper destroys its stack on
exit.
Verification model
There are two verification layers for this pattern:
- deterministic local gate:
./scripts/verify-cdk-synth.shandmake rubric - live deploy-grade smoke:
./scripts/verify-ssr-site-smoke.sh
The live smoke verifier deploys examples/cdk/ssr-site, checks:
- CloudFront root path reaches the Lambda Function URL fallback and returns the SSR body
/marketingand/marketing/aboutstay on S3 HTML behaviors and serve rewrittenindex.htmlobjects/_facetheory/data/home.jsonstays on the raw S3 data path without HTML rewritesPOST /actions/pingbypasses the origin group and reaches Lambda with full method support- the previous
Host-forwarding 403 regression stays covered by exercising the CloudFront-to-Function URL path end to end - asset and direct-S3 delivery work from S3 through CloudFront
- direct Function URL access matches the deployed auth model for the example’s explicit compatibility setting
Run it manually when you want an end-to-end AWS check:
./scripts/verify-ssr-site-smoke.sh
Optional environment:
APPTHEORY_SSR_SITE_STACK_NAMEto override the temporary stack nameAPPTHEORY_SSR_SMOKE_KEEP_STACK=1to skip automatic destroy for debugging
The live smoke verifier is intentionally separate from release automation so the normal release path stays zero-config. Use it as a manual deploy-grade check when you explicitly want a real AWS verification pass.
Example
The canonical runnable stack remains:
examples/cdk/ssr-site/
Use that example plus this guide as the single AppTheory + FaceTheory deployment story.