Your favorite framework in the browser, the same app in a desktop window and Ink in the terminal
FSR compiles your routes, loaders and pages to a plan that a Rust binary runs, with no JavaScript runtime underneath. Pick where it runs below.
What happens at build time
Routes, loaders and actions in TypeScript. Pages are TSX, Tera or MDX, with islands in React, Vue, Svelte or web components. A terminal app's page is Ink.
Loaders and pages become a plan. Islands become ES modules through snapfirec.
Then pick where it runs
How it runs on the web
The host matches the route, runs the loaders and renders the page in Rust.
A route with a loading.tsx or a slot streams the slow part when its loader finishes. Islands hydrate on load, when visible or when idle. Server islands send no component code.
Pages rendered in Rust
Loaders and pages compile to a plan that the Rust host runs. There is no JavaScript engine on the server to start or warm up after a restart.
Tutorial(let count
(reduce (values (session cart)) 0
(fn (n q) (+ n q))))Server islands
The browser downloads no component code for a server island. A click sends the event to the server, which runs the handler and sends back the changed markup.
TutorialStreaming
The page is sent straight away and a slow section is filled in when its data arrives. You don't write a promise or a suspense boundary.
Tutorialapp/routes/
slots/suggestions/
page.loader.ts the slow call
loading.tsx shown until it finishes
product/[id]/
page.loader.ts the fast callSession state
The cart is stored in the session on the server. Adding an item is one typed call and there is no client store to keep in sync.
Tutorialexport interface Session {
cart: Record<string, bigint>;
}
export const defaults: Session = {
cart: {},
};Typed service calls
The API client and its request and response types are generated from an OpenAPI document. A loader calls the service like a function.
Tutorialconst product = await services.shopping.getProduct({ id });
const reviews = await services.reviews.forProduct({ id });Deployment
fsr bundle writes a directory to copy to the server. An app with no crate gets fsr-server in it and an app with a crate adds its own binary. fsr doctor runs checks for failures that only show up in production.
Tutorial$ fsr doctor shop/app doctor 22 checks, nothing to report
Request traces
Each request is recorded as a tree of spans. It shows which loader waited on which service, what ran in parallel and which cache answered.
Tutorial$ fsr dev shop/app fsr server on http://127.0.0.1:3000/ fsr traces on http://127.0.0.1:3000/__fsr/traces
Rendering
Data and state
Sign-in and offline
Shipping and running
Building
Moving an app over
Rust
How it runs on the desktop
Desktop is experimental and needs fsr built with --features desktop. It has been tried on macOS only and there is no packaging or signing yet.
fsr serve --desktop starts the host and opens the system's webview on it. Closing the window stops the host.
The window loads the pages the website serves. An island reaches the machine through the desktop bridge.
The app in a native window
The window opens on the same host the website runs. Its title follows the page's title and the menu bar has Back, Forward and Reload. Under fsr serve --desktop, serve = "scheme" serves it with no port open at all.
Tutorial[desktop] name = "Shop" serve = "scheme" cookies = "disk" $ fsr serve shop/app --desktop
Files from the system dialog
openFile from @snapfire/fsr/desktop opens the system dialog and answers the chosen paths, which an action reads. openExternal hands a link to the system browser. In a browser both reject, so the island can fall back.
Tutorialconst paths = await openFile({
filters: [{ name: "CSV", extensions: ["csv"] }],
});
if (paths) await actions.$root.importProducts({ path: paths[0] });How it runs in the terminal
Terminal mode is experimental and needs fsr built with --features terminal. An app with runs = "terminal" runs under fsr dev or once with fsr run.
The loader runs once, with the command line after -- as params. The root page is an Ink component.
useInput steps the page's state and only the cells that changed are redrawn. No Node and no React run.
An Ink page, lowered
The root page is an Ink component. Box, Text and useInput come from @snapfire/fsr/terminal/ink, which fsr migrate writes in place of ink. The build lowers it to data the terminal runs, so no JavaScript engine starts.
Tutorialexport default function Counter() {
const [count, setCount] = useState(0);
useInput((input) => {
if (input === "+") setCount(count + 1);
});
return <Text bold>Count: {count}</Text>;
}Files, flags and processes
The loader reads the command line and the machine through std. An effect starts git with system.spawn and the pane shows what it printed.
TutorialuseEffect(() => {
let read = "";
const git = system.spawn("git", ["log", "--oneline", "-5"]);
git.stdout.on("data", (chunk) => {
read = read + chunk;
});
git.on("close", () => setLines(read.trim().split("\n")));
}, [runs]);Terminal
Server attack surface
No JavaScript runs on an FSR server, on the web, in the desktop window or in the terminal. npm never runs on your machine either. That removes the classes of bug in the first list. The second list is still yours to handle.
Removed by design
- Deserializing into a JavaScript runtime. On Node: A decoder that builds JavaScript objects from a request runs inside the server's runtime. React2Shell made the RSC decoder run code without sign-in. node-serialize ran any function a payload carried. FSR: A request decodes against the action's typed contract into Rust values. There is no JavaScript heap for a crafted payload to build objects in. Precedents: React2Shell, CVE-2025-55182 · node-serialize, CVE-2017-5941
- Bugs in the runtime and the C it bundles. On Node: Node bundles V8, llhttp, undici, zlib and Brotli. A bug in any of them is in every Node server until Node ships a release and you deploy it. FSR: No Node or Bun on the server, so none of that code runs in the host. Precedents: Node Brotli decoder, CVE-2026-111497
- Prototype pollution. On Node: A request body with a __proto__ or constructor.prototype key that reaches a deep merge writes to the prototype every object in the process shares. That can flip a check like user.isAdmin for every later request. Through a template engine it can lead to code execution. FSR: The IR has no prototypes, so there is no shared object to write through. Precedents: lodash, CVE-2019-10744
- Code injection through eval and templates. On Node: eval, new Function and template engines turn strings into code while the server runs. In ejs 3.1.6 a request option became an operating system command when the template compiled. FSR: The IR is a fixed set of operations written at build time. A request cannot add one. Precedents: ejs, CVE-2022-29078
- npm install scripts on your servers. On Node: A Node deploy runs npm install or npm ci on the machine that builds or serves the app. npm runs every dependency's preinstall and postinstall script there by default, with that machine's credentials. The compromised ua-parser-js installed a cryptominer and a password stealer this way. Shai-Hulud stole npm, GitHub and cloud tokens and published itself into its victims' other packages. FSR: Packages are vendored for the browser and never run in the host. fsr add runs no install script. A package added with --published is checked against the registry's integrity hash. Precedents: ua-parser-js, 2021 · Shai-Hulud, 2025
- Regular expression DoS in routing. On Node: path-to-regexp turns routes into regular expressions. A route with two parameters in one segment backtracked on a crafted URL and held Node's one event loop, so every other request waited. FSR: Routes match through a radix tree with no regular expression. A pattern in your own code that nests unbounded quantifiers is refused instead of run. Precedents: path-to-regexp, CVE-2024-45296
Still yours to handle
- The HTTP layer. hyper, actix and any proxy in front have their own security releases.
- FSR's own code. Its decoders and sign-in flows are Rust, so memory safe, but a logic bug is still a bug.
- Rust build scripts.
cargo buildrunsbuild.rsand proc macros from your Rust dependencies on the build machine. They are never in the request path. - Your authorization. A loader or action that skips a check is not caught by the runtime.
- Your regular expressions. A pattern without nested quantifiers can still backtrack for a long time on a large field. Bound the field or keep patterns simple.
- The browser. Islands and vendored packages are JavaScript and their advisories apply there. CSP is built in and starts report-only.
Patching a dependency
Every server-side dependency is in the Cargo lockfile of the binary that serves the app. cargo audit can check it against the RustSec advisory database. A fix released in a compatible version ships with cargo update -p and a rebuild. A fix that is not released yet ships with a [patch.crates-io] line pointing at the fixed source. Neither waits for a SnapFire release. A bug in FSR's own crates does.
No JavaScript, Just Fast IR
Real sources and the IR the compiler produced from them, regenerated on every build of this site. The last tab is a loader the compiler refuses and the error it reports.
import type { Ctx } from "@snapfire/fsr/host";
export async function load({ session }: Ctx) {
const count = Object.values(session.cart).reduce((n, q) => n + q, 0n);
return { count };
}(let count (reduce (values (session cart)) 0 (fn (n q) (+ n q))))
When to pick FSR and when not to
Where another framework fits your project better, this says which one.
Pick FSR when
- Your app is large. Each team deploys its own app. The host mounts them into one site with one session.
- Your backend is already Rust. Pages are served by the same binary as your API.
- You want no Node on the server. A deploy is one binary and a directory.
- A security review gates your release. No JavaScript runs on the server, so deserialization, prototype pollution and npm supply-chain bugs have nothing to reach. The six classes →
- Server render time matters. 116K product pages a second on one machine, 77x Next.js and 1.2x Leptos.
- Your team writes React, Vue or Svelte. All three render on the server and share one store in the browser.
- Some pages need no TypeScript. Tera templates and web components sit beside React pages.
- You want the same app on the desktop. The window uses the system webview, with no bundled Chromium and no Node.
- Your CLI is written in Ink. fsr migrate brings it over and it runs with no Node. 26 of Ink's 29 examples run unchanged.
Pick something else when
- You need the largest ecosystem and hiring pool. Next.js has it.
- The site is mostly content. Astro ships no JavaScript by default and has CMS integrations.
- You deploy to serverless or the edge. SvelteKit, Nuxt and Astro have adapters for it. FSR runs as a server process.
- You want to write components in Rust. Leptos and Dioxus compile Rust to WebAssembly.
- You need a signed desktop installer today. Tauri and Electron package and sign. FSR's desktop mode is experimental and has been tried on macOS only.
- You need to ship a terminal binary today. Ink on Node does. FSR runs a terminal page under fsr dev or fsr run.
- You need a stable 1.0. FSR releases are 0.x and change between versions.