Skip to content
forma.
v3.0
☰
Browse documentation
Start here
Build interfaces
Native rendering
Tooling
Security and operations
Architecture
Application guides
API reference

Docs/Security and operations

FORMA 3.0

Upgrade to Forma 3.0

Migrate public APIs and rendering contracts to the coordinated 3.0 package set.

View source ↗Markdown ↓Source 753be2a
On this page

This guide describes the published coordinated 3.0.0 APIs. See the as-built summary for actual publication and qualification. It explains an application upgrade; it does not claim that any external application has already migrated.

Move the coordinated set#

Forma now ships from one repository as seven packages at one release version: @getforma/core, @getforma/compiler, @getforma/build, @getforma/create-app, forma-ir, forma-ir-derive and forma-server. Move the application’s direct dependencies and their matching dependency closure together, commit the npm and Cargo lockfiles, and rebuild both assets and server. Stable template manifests use caret requirements; prerelease manifests and the tested archive locks remain exact. Use Node 22 and Rust 1.85 or newer for the templates; repository qualification uses the pinned Node 24 toolchain.

FMIR 3 and manifest schema 2 replace the prior wire formats. Within a major, a reader accepts writers of its own or an older minor; upgrade the renderer first, then compiler/build/core. Patches do not restrict that window. This rule does not make a mixed major or unrelated prerelease compatible. See the compatibility contract, and start template migrations with the preserved-source comparison.

The removed @getforma/core/server, /ssr and /wasm subpaths no longer resolve. Use authenticated application routes for server operations and forma-server for native rendering; "use server" is refused by the build. See the public surface and removed forms.

Bind typed server pages before listening#

Replace the A2 module maps, per-request SlotData and try_render calls with declared TypeScript props and derived Rust handlers. Startup calls load_ir_modules, binds every SSR route with pages.bind::<Props> or bind_with, then calls pages.finalize() before the application's listener. A refused module, incompatible handler or unbound SSR route stops that startup path. Failed finalization is repairable; successful finalization seals every clone and refuses further binds. Handles return RENDER-406 until finalization succeeds.

The original startup helpers now return errors: assets::load_manifest::<A>() returns Result<AssetManifest, String>, assets::asset::<A>(name) returns Result<String, String>, and both CspNonce::generate() and generate_csp_nonce() return Result<CspNonce, NonceError>. Propagate these results with ? or handle the error before starting the server or constructing a response. Missing/non-UTF8 assets, invalid manifests and a failed nonce draw no longer panic through these names. Existing try_asset, try_load_manifest and CspNonce::try_generate callers keep their fallible signatures as compatibility aliases.

A request calls handle.render(&props, &RenderCtx::new()) and handles the returned Result<PageResponse, RenderError>. Return the PageResponse through the framework response adapter so its HTML, nonce, policy and no-store headers stay together. Log/respond to errors in the application; never turn an SSR error into a browser shell. into_parts_unchecked remains the explicitly named exit for integrations that need response parts.

Limits aliases the existing WalkLimits. Defaults remain 8 MiB of walked HTML and 50,000 list iterations. bind_with sets an immutable page default; a context without an override inherits it. with_limits overrides the pair for that request only. Zero is restrictive. Error counts and timing describe the walker; props checking/conversion and document assembly take additional time. Measure the complete handle call before choosing a blocking-pool policy. Forma does not spawn rendering threads.

Move per-request document extensions to RenderCtx: title/body class, typed policy/head/preload settings, with_config_json(Value), with_personality_css(String) and with_body_prefix(RawHtml). JSON and CSS retain their existing escaping. RawHtml still requires the reviewed constructor. A handler accepts neither a caller nonce nor executable config script. Public PageConfig now contains only browser document metadata, with no module, slots or render-mode selector.

An explicit ssr: false route may use render_browser_shell; the route must exist and be declared Browser. Removed APIs are try_render, render_page, render_page_with_nonce, PageOutput, load_ir_modules_strict, public IrModules/RenderMode and the obsolete PageConfig fields. The strict Pages loader is the only public module loader.

Development reload keeps module and asset snapshots paired. Invalid/missing manifests and corrupt modules retain the last accepted pair. Compatible edits reload; schema mismatches return RENDER-405. Adding/removing a route or changing its SSR/Browser mode after finalization requires restart and new bindings, so a running registry cannot acquire an unbound SSR route.

Unwinding faults during the typed request boundary become RENDER-399, and a later request can render. panic = "abort" terminates the process and all in-flight requests instead. Audit the application's deployment profile during migration. A library upgrade does not change that profile.

See declared props, server API and diagnostic codes. Keep the response and startup contracts together while migrating each handler.

Activate declared islands on native HTML#

Register compiler-visible islands with activateIslands({ Counter }). The call now returns Promise<void>: await it when subsequent code requires requested load activation to finish. Preflight stays synchronous; load work may continue in browser tasks, while deferred triggers settle readiness once armed. Default entry calls can ignore the promise. See load scheduling and readiness. Seed their local signals from declared props, not independent Browser defaults. The server emits one bounded, inert head-state envelope, and original island hosts carry their state occurrence and compiled identity. The complete escaped envelope is limited to 1 MiB; do not hand-write the markers or state block.

Activation validates state and the served structure before attaching behavior. It adopts existing nodes without initial DOM repair or initial setter replay. A mismatch leaves that island inert with a coded diagnostic; unrelated valid entries remain eligible. Calling the component entry’s mount() on marked SSR content throws FORMA_E_MOUNT_SSR. The minimal template demonstrates a server count of 7, retained nodes through activation and later 7 → 8 → 0 interaction; explicit Browser mode starts at 0. See island state, ownership and early edits.

Native components require one stable owned Element root. Keep conditional and list ranges inside that root. Native lists use callable sources backed by declared rows or admitted literal defaults, stable supported keys, and a single Element row root. createKeyedRows supports indexed replacement while preserving row ownership; see its API. Use browserOnly for an explicitly client-rendered island body, or declare an entire Browser route when native rendering is not intended.

The native compiler rejects unsupported source even without --strict; the old placeholder/warning escape is gone. Address the reported code and source position using the admitted subset. In particular, boolean-valued text requires an explicit textual representation, conditions must be boolean, and unsupported computed data should become declared presentation props. Attribute omission, boolean presence and string values follow the shared parity rules. Exact className, htmlFor and tabIndex aliases use their native attribute names; duplicate native attribute names, including ASCII case variants in serialized markup, are refused before publication.

Native SVG and MathML attributes must preserve spelling and namespace through HTML parsing. Use canonical SVG names such as viewBox and preserveAspectRatio, MathML definitionURL, and lowercase data-*/aria-*. Mismatched spellings and unsupported XML/XMLNS or nonstandard XLink namespace forms refuse BUILD-610; use a supported attribute or explicit Browser mode. Standard lowercase XLink forms such as xlink:href remain supported on foreign elements. Integration-point children follow their possible namespaces, and this proof does not widen the downstream ASCII-only attribute-name policy. The admitted subset records these finite rules.

Mount HTML-runtime markup explicitly#

The HTML runtime entry no longer starts on import or watches the document. Replace its tierless mount, reconcile, initRuntime, destroyRuntime and global configuration calls with an explicit owner:

ts
import { mountMarkup, trustedHtml } from '@getforma/core/runtime';
const handle = mountMarkup(root, trustedHtml(reviewedMarkup), {
  tier: 'reviewed',
  grants: {},
});
handle.update(trustedHtml(nextReviewedMarkup));
// Dispose before removing/replacing the owned widget, including nested handles.
handle.unmount();

Every mount names its tier. Omitted grants give no fetch, outside-root DOM or navigation authority. Pass any required authority and diagnostic/budget configuration in that mount’s options. trustedHtml marks reviewed raw input; it is not a sanitizer, and unreviewed markup belongs in the opaque-origin frame. Removing a DOM root alone does not dispose its handle. The obsolete data-forma-mount="manual" switch can be removed; it only emits a development migration warning. These removals apply to @getforma/core/runtime; the component entry’s Browser mount() remains available. See the runtime migration.

Keeping the previous release's assets (FORMA-186)#

Keep the dist/ of the release that is live as a release artifact in your pipeline and pass its path to the build as previousDist (another directory than outputDir). The build copies that release's own hashed assets into the new dist/, flagged retained and immutable in the manifest, so a tab opened before a deploy still loads its lazily imported chunks from the one dist/ you embed. One generation only. Mark every release that fixes a vulnerability, a Forma security upgrade included, with security: true (or FORMA_SECURITY_RELEASE=1): it retains nothing. Leave the option out for a first deploy; your first 3.0.0 deploy over a 2.x release retains nothing and says so. Details and the errors (BUILD-820, BUILD-821, BUILD-822) are in packages/build/README.md, "Retention". Unknown FORMA_SECURITY_RELEASE values fail even when security: true already marks the build. Treat the previous artifact store as trusted input: the SHA-384 checks detect changed files, not an attacker who can replace both the files and their manifest.

Full asset digests#

Rebuild schema 2 manifests with complete canonical SHA-384 metadata before starting the reader. Every asset requires integrity, including retained files; a filename hash is not a substitute. Manually constructed Browser manifests follow the same checks. External script, stylesheet and modulepreload tags now include integrity and anonymous crossorigin attributes while retaining their existing nonces. Inline bodies remain nonce-protected. A typed preload no longer present in the response's accepted manifest returns RENDER-104, so development reloads must refresh that table rather than emitting an unchecked reference.

Static routes and explicit Browser mode#

An explicit SSR manifest route with js: [] must have a valid IR module with no declared islands and an empty modulepreload list. Missing JS metadata is an error, not a static declaration. LOAD-109 refuses an island module without browser code; MANIFEST-103 refuses contradictory static preloads. The development reader checks changed manifest policy even for unchanged IR bytes and keeps both parts of the last valid pair on failure.

Do not pass with_config_json or a stale/nonempty module-preload table to a static SSR handle: these return RENDER-105. Configuration remains a nonced window.__FORMA_CONFIG__ assignment on JS-enabled SSR and Browser routes; it is not moved into the inert island state channel. CSS/fonts, branding, strict headers and the inert empty state block remain. Explicit executable RawHtml is outside zero-JS qualification.

Enabled worker registration now requires a page JS entry. Static pages do not newly install workers; serving /sw.js and the off-mode retirement path remain unchanged for existing clients. Keep the endpoint while changing worker policy. The compiler removes a native page’s whole-page mount and drops its browser entry when there are no islands. Browser routes explicitly declare ssr: false; unsupported native source never silently turns into a Browser route. Rebuild and restart when changing a route’s mode.

Deployment identity, qualification and rollback#

Build and deploy assets, manifest and native modules from the same accepted output. Before mount or activateIslands, the bundle compares its identity to the page root. A mismatch leaves the page inert and dispatches the cancelable Document event forma:skew with { page, bundle, reload }. Its automatic reload is bounded to once per full page identity when storage is available. Public direct hydrateIsland and application-owned dynamic imports remain outside that guard; the application handles its own failed imports. See the deployment contract.

Keep the scripts-off install policy and registry signature checks, and inspect the build’s actual size report. The framework's own release fixtures enforce the L-64 ceiling of 28,100 gzip bytes of attributed Forma code; this is not a whole-application size maximum or a substitute for browser measurements. Package bundle caps remain separate. Framework release qualification includes R17, packed templates, parity and P17. A successful application build alone does not reproduce those checks.

The 3.0.0 release's independently audited RC and final qualification are complete. L-66-QUOTA scheduled its active burn-in after final publication; the later receipt verifies 3,660 active seconds with zero faults, while the immutable original record retains its then-deferred status. See the as-built summary and burn-in procedure. This historical exception does not waive verification for a future release.

For rollback, restore the previously tested source, both lockfiles and matching deployment artifacts together. Do not combine an old reader with FMIR 3 or an old manifest with a new browser bundle. Preserve one previous deployment’s artifacts where the retention policy permits it; security releases intentionally retain none. Consult the support policy for the release family’s published maintenance window.

Find your next answer.

Search Forma 3.0 documentation, guides and examples.