FaceTheory Migration Guide
This guide focuses on the supported migration paths into the current FaceTheory runtime contract and deployment model.
When To Use This Guide
Use this guide when you are:
- replacing an ad hoc SSR handler with FaceTheory entrypoints
- introducing SSG or ISR into an SSR-only FaceTheory app
- updating AppTheory or TableTheory dependency pins
Scope Guardrails
- Keep migration steps user-facing and task-oriented.
- Prefer published package exports and documented deployment conventions.
- Do not claim support for an unpinned AppTheory or TableTheory combination.
- Use the current package exports, examples, and deployment docs as the migration baseline for this repository.
Migration 1: Ad Hoc Handler To Canonical AWS Entrypoint
Use this path when the current app still owns request translation or response shaping outside the FaceTheory runtime. It replaces custom handler glue with the documented FaceTheory entrypoints.
- Inventory the current handler surface.
- Move request routing into
createFaceApp({ faces }). - Expose the app through one of the supported entrypoints:
createLambdaUrlStreamingHandler({ app }), or- AppTheory
createLambdaFunctionURLStreamingHandler(app)pluscreateAppTheoryFaceHandler({ app })
- Re-run local verification.
Validation:
cd ts
npm run typecheck
npm test
npm run example:streaming:serve
Migration 2: SSR-Only Routes To Mixed SSR, SSG, And ISR
Use this migration when route freshness requirements have diverged and a single SSR-only mode is no longer the right fit. It helps reclassify routes before you change any deployment wiring.
- Review each route and assign the correct
FaceMode. - For build-time routes, switch to
mode: 'ssg'. - For cacheable pages that need periodic regeneration, switch to
mode: 'isr'and setrevalidateSeconds. - Keep
mode: 'ssr'for request-time or personalized pages. - For dynamic SSG routes, add
generateStaticParams().
Validation:
cd ts
npm run example:ssg:build
npm run example:ssg:serve
Migration 3: Introduce Production ISR Safely
Use this path only after you are ready to provision and verify the storage and lease coordination that blocking ISR depends on. It treats ISR as an infrastructure-backed runtime mode, not a flag flip.
- Provision HTML storage through
S3HtmlStore. - Provision metadata and lease storage through
createTableTheoryIsrMetaStore({ config })or anotherIsrMetaStore. - Configure
htmlPointerPrefixintentionally. - Confirm cache headers and
x-facetheory-isrstates on a known route.
Validation:
curl -I https://<cloudfront-domain>/isr-demo
Expected:
x-facetheory-isr: miss|hit|wait-hit|stale
Migration 4: Adopt ISR Tenant Fail-Closed Defaults
Use this path when upgrading an app that already has ISR routes and request traffic may carry tenant boundary
headers such as x-tenant-id or x-facetheory-tenant. FaceTheory now fails closed before ISR metadata lookup or
HTML writes when those headers reach an ISR route without an explicit isr.tenantKey or custom isr.cacheKey.
This prevents tenant-varying HTML from silently sharing the default ISR cache partition.
- Classify every ISR route:
- Tenant-invariant routes do not read tenant headers, auth headers, cookies, or other request-varying tenant state while rendering cached HTML.
- Tenant-varying routes render different HTML for different tenants or request-scoped identities.
- For tenant-invariant ISR, strip viewer-supplied tenant-like headers at the CloudFront/AppTheory boundary before the request reaches FaceTheory.
- For tenant-varying pages that are personalized or permission-sensitive, prefer
mode: 'ssr'unless the rendered HTML is safe to cache independently per tenant. - For tenant-varying ISR that is safe to cache, configure an explicit partition:
- use
tenantKey: tenantKeyFromTrustedHeader('x-tenant-id')only after CloudFront/AppTheory has stripped viewer-supplied values and injected trusted tenant context, or - provide a custom
cacheKeythat includes every request-varying dimension that affects the HTML.
- use
- Do not trust tenant identity from a raw viewer header. Treat tenant headers as deployment-internal context after the trusted boundary has normalized them.
Validation:
# tenant-invariant route: no tenant headers reach FaceTheory
curl -I https://<cloudfront-domain>/isr-demo
# explicit trusted partition: tenant A and tenant B must not share the same cached HTML
curl -I -H 'x-tenant-id: tenant-a' https://<cloudfront-domain>/tenant-isr-demo
curl -I -H 'x-tenant-id: tenant-b' https://<cloudfront-domain>/tenant-isr-demo
Expected:
- tenant-invariant ISR returns
x-facetheory-isr: missand thenhitorwait-hiton repeat requests - unpartitioned ISR with tenant boundary headers returns a deterministic server error and does not write ISR metadata or HTML cache entries
- explicitly partitioned ISR keeps tenants separated and still reports normal
x-facetheory-isrtransitions
Rollback:
- switch affected tenant-varying routes back to
mode: 'ssr', or - strip tenant-like headers for routes proven to be tenant-invariant before they reach FaceTheory.
Migration 5: Align Upstream Dependency Pins
Use this cleanup when dependency installation drift is the main risk and runtime wiring is otherwise already in the supported shape. It restores the exact AppTheory and TableTheory combinations validated by this repo.
- Replace floating installs with exact GitHub release tarballs.
- Keep
ts/package.jsonpins and overrides synchronized. - Update docs when pins change.
Validation:
cd ts
npm ci
npm run typecheck
npm test
Migration 6: Replace App-Local OAC Form Workarounds
Use this path when an SSR control-plane page behind AppTheorySsrSite Lambda Function URL OAC has an app-local fetch
shim, disabled form, direct Lambda Function URL action, or temporary Function URL auth rollback because native browser
forms could not provide x-amz-content-sha256.
- Keep the public form action on the same-origin CloudFront URL.
- Route the mutating action path to Lambda/AppTheory with
ssrPathPatternswhen the deployment has an S3 or SSG/ISR origin path that could otherwise intercept the request. - Mark only supported URL-encoded forms with
data-facetheory-oac-form. - Install
startAwsOacFormTransport()from the Face’s client bootstrap module. - Remove any direct Function URL form action or app-local transport workaround after the FaceTheory helper is verified.
- Keep authentication, authorization, CSRF, idempotency, and business validation in the application layer; the
x-amz-content-sha256value is only AWS signing plumbing. - Leave browser-generated multipart uploads out of this migration unless a separately scoped transport constructs and hashes the exact multipart bytes.
Validation:
cd ts
npx tsx test/unit/oac-form.test.ts
Deployed verification:
curl -I https://<cloudfront-domain>/control/items/new
Then submit the marked form in a browser through the CloudFront URL and confirm:
- the request reaches the AppTheory/FaceTheory Lambda instead of failing with
InvalidSignatureException - the request includes
x-amz-content-sha256andcontent-type: application/x-www-form-urlencoded;charset=UTF-8 - unsupported marked encodings fail closed before sending
- Lambda Function URL auth remains
AWS_IAMbehind CloudFront OAC
Rollback:
- remove the
data-facetheory-oac-formmarker or the bootstrap call to return to native browser behavior while the app remains pinned to the previous known-good FaceTheory release tarball, or - temporarily disable the affected mutating form in the consuming app.
Do not make ssrUrlAuthType: NONE a durable rollback. If an operator explicitly authorizes it to recover a broken
deployment, record an owner, expiration date, and restoration plan back to AWS_IAM + OAC.
Migration 7: Move Legacy Inline Hydration To Strict CSP Hydration Sidecars
Use this path when a route currently relies on inline __FACETHEORY_DATA__, inline style output, or raw head HTML and
now needs to satisfy a strict no-inline CSP.
- Classify the route’s policy:
- Nonce-compatible SSR can keep FaceTheory-owned inline hydration/styles only when each request receives a
unique
FaceRequest.cspNonceand the response CSP header carries the matching nonce. - Strict no-inline must set
csp: { inlineScripts: false, inlineStyles: false, rawHead: false }and move scripts, CSS, and hydration data to same-origin external resources.
- Nonce-compatible SSR can keep FaceTheory-owned inline hydration/styles only when each request receives a
unique
- Pick the sidecar owner by delivery mode:
- SSR framework-owned sidecars: keep
viteHydrationForEntry(manifest, entry, data)in the Face and configurecreateFaceApp({ ssrHydrationSidecars: { htmlStore, signingSecret } }). FaceTheory stores the exact render-time payload once, emits a/_facetheory/ssr-data/...URL, and serves it through the same FaceApp handler without re-runningload()orrender(). - Caller-managed external sidecars: replace the inline hydration with
externalHydrationForEntry(manifest, entry, data, { dataUrl })only when the application owns that same-origin JSON route or object and can serve the exact payload used for the HTML render. - SSG sidecars: let the SSG build write static strict hydration JSON under
/_facetheory/data/*and route that prefix to S3 beside the generated HTML. - ISR sidecars: rely on the ISR runtime to pair strict hydration data with the cached HTML and metadata. Do not
route ISR hydration through the SSR
/_facetheory/ssr-data/*prefix.
- SSR framework-owned sidecars: keep
- Route the sidecar URL from the same origin as the page:
/_facetheory/data/*is the static SSG sidecar namespace and should route to S3/CloudFront static delivery./_facetheory/ssr-data/*is the framework-owned SSR runtime sidecar namespace and must route to the same Lambda/FaceApp handler as the SSR HTML.- Caller-managed URLs should use a distinct application-owned prefix and must not recompute request-dependent hydration on a later sidecar request.
- Move CSS into the Vite client entry and emit assets with
viteAssetsForEntry(...)instead of inline<style>tags. - Replace raw head HTML and framework-specific head shortcuts with FaceTheory structured
headTags. - For Svelte strict pages, avoid
<svelte:head>raw SSR output and component<style>fallback output; use external CSS imported by the client entry. - For React streaming strict pages, use
styleStrategy: "all-ready"and avoid Emotion/AntD inline style extraction on routes withinlineStyles:false. - In the client bootstrap, import
loadFaceHydrationData()from@theory-cloud/facetheory/clientand call it before hydrating. If the route uses SPA-style navigation, exporthydrateFaceNavigation(context)and confirmstartFaceNavigation()loads external hydration data before it mutates the current document.
Validation:
cd ts
npm run example:vite:svelte:strict-csp:build
node --import tsx test/unit/strict-csp-harness.test.ts
node --import tsx test/unit/vite-strict-csp-svelte-example.test.ts
Deployed verification:
- request the HTML document and confirm the CSP header is attached explicitly
- confirm the HTML contains
<link rel="facetheory-hydration" ...>rather than__FACETHEORY_DATA__ - fetch the referenced hydration sidecar through the same CloudFront origin and confirm it is same-origin JSON:
/_facetheory/data/*should reach S3 for SSG output, and/_facetheory/ssr-data/*should reach the same Lambda/FaceApp handler for SSR runtime sidecars - confirm external module, CSS, and asset URLs are routed through the documented S3/Lambda behavior split
- run a browser hydration smoke and check for hydration warnings or strict-CSP console errors
Rollback:
- keep the previous FaceTheory release tarball available until strict-CSP output is verified in the consuming app
- if a route cannot yet remove inline styles or inline hydration, leave it on the nonce-compatible SSR path and do not claim it is strict no-inline
- do not weaken the deployment CSP or publish a strict-CSP release claim without matching runtime, RC, and deployment evidence
Rollback Notes
- Keep the prior handler or deployable artifact available until the new path is verified.
- If ISR rollout introduces instability, revert affected routes to
mode: 'ssr'while storage or lease settings are corrected. - Keep environment-specific rollback commands in the operator runbooks that own the deployed stack.