Start by choosing where the initial HTML comes from. A browser application can use
@getforma/core with a JavaScript bundler. Native rendering additionally uses
@getforma/compiler, @getforma/build, forma-ir and forma-server.
The official packages used by this site are pinned to 3.0.0.
Choose the rendering mode explicitly#
The minimal template has two build commands. Its ordinary build creates Browser routes; the SSR command compiles native pages. From a generated application's root:
cd admin
npm ci
npm run build:ssr
cd ..
CARGO_BUILD_JOBS=3 cargo run --lockednpm ci installs the locked frontend dependencies; build:ssr emits native FMIR
and its paired browser assets. The Cargo command then builds and starts the Rust
server with those files. Run the steps in this order after changing page source.
The template listens on localhost:3000 by default. Request the page with a browser and inspect its source: the server-rendered counter starts at 7 before the browser bundle runs. The template's Rust handler supplies that 7. Clicking changes only the current page's signal; refreshing requests 7 again because no save exists. A Browser build starts its separate client counter at 0. Changing a build flag is a change to rendering behavior, not just an optimization.
Keep the page owner and route aligned#
This excerpt shows the Home portion of a native build, using the minimal template's paths. Keep the template's other entries, such as About, in your actual configuration. Paths are relative to the build working directory.
import { build } from "@getforma/build";
await build({
app: "my-forma-app",
entryPoints: [{ entry: "src/home/app.tsx", outfile: "home.js" }],
cssEntries: [{ input: "src/styles/app.css", outfile: "app.css" }],
routes: { "/": { js: ["home"], css: ["app"] } },
outputDir: "dist",
ssr: true,
ssrEntryPoints: { home: "src/home/app.tsx" },
diagnosticsJson: "ssr-diagnostics.json",
});The home owner connects the browser entry, native entry and route. It also names
the Rust handle: pages.bind::<HomeProps>("home"). It is not a URL. Keep the owner
in the input route even if the page has no islands: the build removes unnecessary
JavaScript from the output itself. An input with js: [] loses that owner association.
Declare request data at the boundary#
Declare page props in TypeScript and derive matching PageProps in Rust. Validate
request data before creating those props. Load the manifest, require the routes
you expect to be SSR, load modules, bind all handles and call finalize() before
opening the listener. Then return the complete PageResponse for each request.
The result should already contain the requested values in its HTML; an island
then adds browser interaction. Persistence requires a separate validated write
path, as demonstrated by Field Notes.
The declared props guide explains supported shapes. The native subset defines which expressions the compiler can represent. Move arbitrary business calculations into Rust and pass explicit presentation values when a native expression is unsupported.
Separate public configuration from secrets#
Browser bundles and rendered props are visible to visitors. Database credentials, signing keys and other server secrets belong in the server process environment, not page props or build substitutions. Environment variable names belong to your application; Forma does not turn every environment variable into a public setting.
The website's native origin uses HOST, PORT and SITE_DIST. The published minimal
template uses FORMA_PORT. Use the names implemented by the application you run.
Diagnose the boundary that failed#
| Symptom | First evidence to inspect |
|---|---|
| Native build refused | Diagnostic code and original source location; check the admitted subset |
| Server refuses startup | Required SSR route, matching manifest/modules, props schema and finalize result |
| Initial data is wrong | Raw HTTP response and validated request props |
| Island does not respond | Browser errors, response policy, paired bundle and actual input behavior |
| Open tabs break after deployment | Build identity and retained assets; never paste a new bundle over an old manifest |
Keep emitted diagnostics with the build. Unsupported native source always fails in
3.0; strict: false does not enable a silent browser fallback. See the
build reference for the complete configuration.