Forte Project Structure
A Forte project created by forte init <name> has the following layout:
<name>/
├── Forte.toml # Project config (project_id written here by forte deploy)
├── cron.yaml # (optional) Scheduled cron job definitions
├── env.yaml # (optional) Shared env vars, secrets encrypted; managed via `forte env set`
├── env.local.yaml # (optional) Local-only plaintext overrides; gitignored, never bundled
├── .gitignore
│
├── rs/ # Rust backend (compiles to WASM)
│ ├── Cargo.toml # [workspace] + [package]; crate-type = ["cdylib"]; standalone (no parent workspace)
│ ├── build.rs # Calls forte_codegen::generate_routes() and generate_env()
│ ├── .cargo/
│ │ └── config.toml # Sets build target = "wasm32-wasip2"
│ ├── .gitignore
│ ├── public/ # (optional) Static files embedded at build time and served verbatim
│ │ └── app-ads.txt # Example: served at /app-ads.txt
│ └── src/
│ ├── lib.rs # Contains the FORTE-MANAGED block (do not edit the block)
│ ├── route_generated.rs # Auto-generated by build.rs (do not edit)
│ ├── env_generated.rs # Auto-generated by generate_env() from the env files (do not edit)
│ ├── pages/ # Page handlers
│ │ └── index/
│ │ └── mod.rs # pub async fn handler(...)
│ ├── apis/ # API endpoints (JSON, no SSR)
│ ├── actions/ # Server actions (called from frontend)
│ │ └── mod.rs # Auto-generated module declarations
│ ├── hooks/ # Self-invoke hooks
│ ├── queue_task/ # Background queue tasks
│ │ └── mod.rs # Auto-generated
│ └── admin/ # Admin tasks
│ └── mod.rs # Auto-generated
│
├── fe/ # TypeScript/React frontend
│ ├── package.json
│ ├── tsconfig.json
│ ├── forte.config.ts # (optional) Vite config extension (mergeConfig'd over generated config)
│ ├── src/
│ │ ├── app.tsx # Head component: exports Head() returning JSX for <head> (shared across all pages)
│ │ ├── paths.generated.ts # Auto-generated path helpers
│ │ ├── pages/ # React page components
│ │ │ └── index/
│ │ │ ├── page.tsx # Default export: the React component
│ │ │ └── .props.ts # Auto-generated from Rust Props type
│ │ ├── actions/
│ │ │ └── .generated/ # Auto-generated by forte-rs-to-ts (do not edit)
│ │ │ └── <name>.ts # Typed Zod client for the action
│ │ └── hooks/
│ │ └── .generated/ # Auto-generated by forte-rs-to-ts (do not edit)
│ │ └── <Name>.ts # React Suspense hook for the hook
│ ├── .forte/ # Auto-generated by forte dev/build (do not edit; git-ignored)
│ │ ├── server.tsx # SSR entry point
│ │ ├── client.tsx # Client hydration entry
│ │ ├── vite.config.ts # Generated Vite config (imports forte.config.ts if present)
│ │ ├── forte-react.ts # Runtime: useForteHook, callAction (aliased as @forte/react)
│ │ └── routes.generated.ts # Frontend route definitions
│ └── public/ # Static assets
│
├── dist/ # Build output (created by forte build; git-ignored)
│ ├── backend.wasm # Compiled Rust backend
│ └── server.js # SSR bundle
│
└── .forte/ # Auto-managed by forte dev (do not edit; git-ignored)
└── data/
├── <sqld files> # Local libSQL database
└── objects/ # Local object storage (served as S3-compatible API)
Key Conventions
rs/src/lib.rs — Managed Block
The section between the FORTE-MANAGED START and FORTE-MANAGED END markers is rewritten by forte build. Do not edit it manually:
// === FORTE-MANAGED START ===
// Auto-managed by `forte build`. Do not edit between the START/END markers.
pub mod actions; // present only when src/actions/ has handlers
pub mod admin; // present only when src/admin/ has handlers
pub mod queue_task; // present only when src/queue_task/ has handlers
mod route_generated;
pub use route_generated::enqueue; // present only when queue tasks exist
// === FORTE-MANAGED END ===
Adding user-defined modules
Everything in rs/src/lib.rs outside the FORTE-MANAGED block is yours to edit. Add shared logic there:
// rs/src/lib.rs
// === FORTE-MANAGED START ===
// ... (do not touch)
// === FORTE-MANAGED END ===
// Your shared modules go here, outside the managed block:
mod db; // e.g. database helpers shared by multiple handlers
mod auth; // e.g. session validation utilities
Handler files inside pages/, actions/, hooks/, queue_task/, and admin/ reference the crate root normally:
// rs/src/pages/index/mod.rs
use crate::auth::verify_session;
rs/src/route_generated.rs — Auto-generated Router
Generated by build.rs (via forte_codegen::generate_routes()). Contains the WASM export wiring, the HTTP dispatch function, and all route matches. Never edit this file by hand.
fe/src/paths.generated.ts — Auto-generated Path Helpers
Generated by forte-codegen alongside route_generated.rs. Exports a paths object with type-safe path builders:
import { paths } from "./paths.generated";
paths["/"](); // "/"
paths["/post/:id"]({ id: "42" }); // "/post/42"
fe/src/pages/<route>/.props.ts — Auto-generated Props Types
Generated by forte-rs-to-ts from the Rust Props enum/struct in the corresponding page handler. Import them in the React component to get type-safe props.
rs/src/env_generated.rs — Environment Accessors
forte_codegen::generate_env() (called from build.rs by default) reads env.yaml and env.local.yaml and generates rs/src/env_generated.rs with a typed accessor per variable:
// env_generated.rs (do not edit)
pub fn cookie_secret() -> &'static str { ... }
pub fn turso_url() -> &'static str { ... }
mod env_generated; sits in lib.rs outside the FORTE-MANAGED block. Values are read from the real environment at runtime; the env files only determine which functions exist. See codegen.md for details.
env.yaml — Shared Environment Variables (optional)
env.yaml stores environment variables bundled into the deploy and injected at runtime. It is meant to be committed: plain entries are readable, and secrets are encrypted with a project key that only the fn0 vault can open, so cloning the repo is enough to deploy. Entries can be plain strings or encrypted secrets:
# env.yaml
PUBLIC_API_URL: https://api.example.com
DATABASE_PASSWORD:
secret: <ciphertext> # written by `forte env set --secret`
Manage entries with:
| Command | CLI | Description |
|---|---|---|
forte env set <key> <value> | forte | Add or overwrite a plain entry |
forte env set <key> <value> --secret | forte | Add an encrypted secret (requires forte login) |
fn0 env list | fn0 | List all entries with their kind |
fn0 env unset <key> | fn0 | Remove an entry |
forte env only implements set. To list or remove entries, use the fn0 CLI (cargo binstall fn0-cli). Both CLIs operate on the same env.yaml file.
env.local.yaml — Local Overrides (optional)
Same YAML shape as env.yaml, but plaintext only — secret: entries are rejected. forte init gitignores it.
forte dev resolves its environment as plain env.yaml entries, overlaid with env.local.yaml. So shared non-secret settings need no duplication; put in env.local.yaml only what differs locally.
Encrypted env.yaml entries cannot be decrypted offline (that needs the vault, i.e. credentials and network), so forte dev leaves them unset and lists them on startup. Give any secret you need locally a plaintext value in env.local.yaml.
fe/src/app.tsx — Head Component
app.tsx exports a named Head function (no default export, no props) whose return value is rendered as the <head> content for every page. The SSR runtime calls <Head /> once per request before writing the HTML stream.
forte init generates:
// fe/src/app.tsx
export function Head() {
return (
<>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
</>
);
}
Rules:
- Must be a named export
Head(not a default export) - Must take no props
- The return value is inserted inside
<head>…</head>— do not return a<head>element itself
fe/forte.config.ts (optional)
If fe/forte.config.ts exists, its default export is merged over the generated fe/.forte/vite.config.ts via Vite's mergeConfig. Use this to add Vite plugins, aliases, or other build settings without editing generated files:
// fe/forte.config.ts
import type { UserConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
export default {
plugins: [tailwindcss()],
} satisfies UserConfig;
Forte.toml
forte init writes an empty Forte.toml as a project marker. After the first forte deploy, the control-plane-assigned project ID is written back:
project_id = "some-unique-id"
The project_id field is the only recognized key. The project's display name is chosen interactively during the first forte deploy (or via --name) and stored in the control plane, not in Forte.toml.
forte deploy and forte destroy edit only the project_id key in place — unrecognized keys, tables, and formatting are left untouched. forte destroy removes the key so the next forte deploy registers a new project.
cron.yaml (optional)
Place this file in the project root to schedule queue tasks on fn0 Cloud. Registered during forte deploy.
- function: send_digest_email
every_minutes: 60
Each function must match a rs/src/queue_task/<name>.rs file whose Input is empty (unit struct or pub type Input = ();). See forte/cli.md#cron-jobs.
rs/public/ — Embedded Static Files (optional)
Files placed in rs/public/ are embedded into the WASM binary at compile time via include_bytes! and served verbatim at their corresponding paths. For example:
| File | Served at |
|---|---|
rs/public/robots.txt | /robots.txt |
rs/public/app-ads.txt | /app-ads.txt |
rs/public/.well-known/apple-app-site-association | /.well-known/apple-app-site-association |
rs/public/images/logo.png | /images/logo.png |
Content-Type is inferred from the file extension:
| Extension | Content-Type |
|---|---|
.txt | text/plain; charset=utf-8 |
.json | application/json |
.xml | application/xml |
.html | text/html; charset=utf-8 |
.webmanifest | application/manifest+json |
.ico | image/x-icon |
.png | image/png |
.svg | image/svg+xml |
apple-app-site-association (no ext) | application/json |
| other | application/octet-stream |
Static file routes are matched before page and API routes in the generated dispatcher, so they cannot be shadowed by a page handler at the same path.
Changes to rs/public/ trigger a rebuild automatically (Cargo watches the directory via cargo:rerun-if-changed=public). No mod.rs or other Rust code is needed — simply placing files in rs/public/ is sufficient.
Note:
rs/public/embeds files in the backend WASM binary — suitable for small, non-cacheable files likerobots.txt,app-ads.txt, and.well-known/payloads. For large assets (images, JS, CSS) that benefit from CDN caching, put them infe/public/instead (served via the Vite build and uploaded to the fn0 Cloud CDN byforte deploy).
rs/Cargo.toml
rs/Cargo.toml is both the workspace root and the package definition in a single file:
[workspace] # makes rs/ a self-contained workspace; prevents being pulled into parent workspaces
[package]
name = "my-app"
edition = "2024"
[lib]
crate-type = ["cdylib"] # produces backend.wasm
The [workspace] section without a members key means the workspace contains only the rs/ crate itself. Do not add rs/ as a member of any parent workspace.
Dependencies added by forte init:
forte-sdk— runtime libraryforte-json— JSON serializerdoc-db(fn0-doc-dbon crates.io) — database clientobject-storage(fn0-object-storageon crates.io) — object storage clientforte-codegen(build-dependency) — code generation