Install
@forst/cli first. Add a postinstall script that runs forst generate ..
Run generate once before you import @forst/gen.
Details are on the Installation page.Compiler-owned $ names
forst generate prefixes Forst-domain exports with $ so generated JavaScript stays valid even when Forst names collide with JS reserved words (class, export, …).
Call functions through the package handle:
$auth.VerifyPassword(input).
Runtime _tag strings and Effect service tags keep normal package paths without $, for example @forst/gen/auth/BcryptGenerateFailed and @forst/gen/auth.
Transport and index infrastructure (InvokeCallOptions, ForstClientConfig, @forst/errors catalog classes) is not prefixed.
What you get
Each Forst package becomes a subpath under@forst/gen:
Types for that package export from the same subpath. Shared shapes also re-export from
@forst/gen.
forst generate writes node_modules/@forst/gen → .forst/client so the specifier resolves. Full output modes and every generate field are on the CLI reference.
Bundlers (Vite, Remix, webpack)
The generated package is a real npm-style module: subpath imports like@forst/gen/auth resolve through package.json exports to files under dist/pkg/. TypeScript and Node resolve it from node_modules/@forst/gen after forst generate creates the link.
The generated package.json declares an optional peer on @forst/cli so real-server helpers under @forst/gen/$testing can load @forst/cli/invoke. Install it only when those helpers are used.
Do not add a bundler resolve.alias that maps @forst/gen to .forst/client. Plain directory aliases skip the exports map, so @forst/gen/main incorrectly becomes .forst/client/main (which does not exist). Let normal package resolution handle @forst/gen.
For SSR frameworks (Remix, Nuxt, etc.), add @forst/gen to ssr.noExternal (or your bundler’s equivalent) so server bundles include the generated client. No tsconfig paths entry and no resolve.alias for @forst/gen are required when the node_modules link is present.
Working reference: examples/in/rfc/bridge-interop/remix-serve (vite.config.ts).
Runtime targets
Default output is for Node.js 20.19+ on the server (SSR loaders, API routes, background jobs). The transport usesfetch, process.env, and Node timing APIs.
Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy) and browser bundles are not supported by the generated client today. Run forst generate in a Node boundary and call Forst from a supported backend. See Call Forst over HTTP § Runtime support.
Monorepos and multiple Forst projects
When to run forst generate . vs forst generate app
Running
forst generate . from the monorepo root when ftconfig.json lives under app/ picks the wrong discovery root and places .forst in the wrong tree. Preferred patterns:
- From the app package:
"postinstall": "forst generate ."in that workspace package’spackage.json. - From the monorepo root:
pnpm --filter @acme/app exec forst generate .(filter sets cwd to the boundary), orforst generate appwhenapp/is the boundary path.
One .forst tree per boundary
Every ftconfig.json boundary gets one compiler-owned .forst/ directory under that boundary root. The default client output is .forst/client. Do not add a second .forst tree manually. A second Forst project needs a second boundary (another ftconfig.json at its project root).
A Forst package named client is fine. It emits to dist/pkg/client.js and resolves as @forst/gen/client. That is separate from the compiler-owned .forst/client output directory.
How the node_modules link is placed
forst generate walks up from the ftconfig boundary root until it finds a node_modules directory, then writes:
.forst-generated marker inside the output records which boundary owns the link. If the link already points at the correct output, generate leaves it untouched.
Treat the link like .forst itself: regenerated infrastructure, not something npm installed. postinstall: forst generate . restores it after npm ci deletes node_modules.
Single app in a monorepo
When one boundary lives inside a larger repo (for exampleapps/remix-serve/), the link usually lands in that app’s nearest node_modules. Hoisting to the repo root only changes where the link is created, not what it points at. Each app’s link still targets that boundary’s .forst/client.
Two Forst boundaries, same repo
If two boundaries both use the default name@forst/gen and share a hoisted node_modules, the second forst generate fails with an ownership error naming both boundary paths. That is intentional. Silent overwrite would point both apps at one generated client.
Pick one fix:
Committed mode example:
packages/forst-client in the workspace. TypeScript and bundlers resolve it without a symlink. You still run forst generate when .ft sources change.
Recommended pnpm monorepo hooks
- Put
ftconfig.jsonand.ftsources in the workspace package that owns the Forst boundary (for exampleapp/orapps/api/). - In that package’s
package.json:"postinstall": "forst generate .". - Optional
"predev"/"prebuild": same command when generate must run before local servers or builds without relying on install alone. - Root scripts may call
pnpm --filter <boundary-package> run postinstall(or a namedgeneratescript) so CI from the repo root still targets the correct boundary. - For pnpm strict / isolated linking issues, use committed mode (
outDir+link: "never") below. Do not add a second ephemeral link strategy.
Package managers and environments
Nested packages without their own ftconfig.json
Commands run from a subdirectory still discover the ancestor ftconfig.json. One boundary, one .forst tree, one link. No extra setup.
Types from .ft
Define the shape once in Forst:- Forst
- Generated Go
- Generated TypeScript
Discovery with ftconfig.json
forst generate and forst dev share include/exclude rules from ftconfig.json:
files.include selects which .ft files enter generate and dev. It does not override Go package layout. Each directory holds one Forst package name. Broad includes like **/*.ft at a monorepo-root boundary pull every nested package under that boundary. Prefer placing the boundary at the app that owns the .ft tree.
Pass -config explicitly or let the CLI search upward from your target directory.
Lifecycle scripts (postinstall, optional predev / prebuild) live on
Installation § Generated TypeScript client.
End-to-end example
See the tictactoe exampleexamples/in/tictactoe/ and examples/client-integration/.
Caveats
Structure only in TypeScript
Generated types reflect field names and TypeScript shapes. They do not carry runtime rules likeString.Min(1) or Int.Max(99). Add client validation yourself, or call the Forst server which validates at the boundary.
Provider gated exports
Functions that need wiredProviders(f) are excluded from TypeScript emit. The invoke wire stays payload only. See Providers § Caveats.
Nominal error tags
TypeScript_tag export for nominal Forst errors is still maturing. Treat those payloads as evolving. Invoke transport failures (InvokeRejected, …) already use stable tags. See Errors and Result § Caveats and Call Forst over HTTP.
Related
Installation
@forst/cli, postinstall, and first generate.Call Forst over HTTP
Invoke generated functions over HTTP.
Testing
Stub calls with
withForstTestScope.Effect mode
generate.effect and ForstClientLive.CLI reference
Full
generate configuration table.