SnapFire FSR

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.

Build once

What happens at build time

01 · buildWrite

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.

02 · buildCompile

Loaders and pages become a plan. Islands become ES modules through snapfirec.

Then pick where it runs

web

How it runs on the web

03 · requestRun

The host matches the route, runs the loaders and renders the page in Rust.

04 · requestRespond

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.

render

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
compiled from page.loader.ts
(let count
  (reduce (values (session cart)) 0
    (fn (n q) (+ n q))))
render

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.

Tutorial
a server island, live
Served by snapfire_fsr_host
render

Streaming

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.

Tutorial
a slot with a loading.tsx
app/routes/
  slots/suggestions/
    page.loader.ts   the slow call
    loading.tsx      shown until it finishes
  product/[id]/
    page.loader.ts   the fast call
data

Session 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.

Tutorial
app/schemas/session.ts
export interface Session {
  cart: Record<string, bigint>;
}

export const defaults: Session = {
  cart: {},
};
data

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.

Tutorial
a loader
const product = await services.shopping.getProduct({ id });
const reviews = await services.reviews.forProduct({ id });
ship

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
$ fsr doctor shop/app
doctor       22 checks, nothing to report
ship

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
$ 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

desktop

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.

03 · launchRun

fsr serve --desktop starts the host and opens the system's webview on it. Closing the window stops the host.

04 · in the windowRespond

The window loads the pages the website serves. An island reaches the machine through the desktop bridge.

desktop

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
config/app.toml
[desktop]
name = "Shop"
serve = "scheme"
cookies = "disk"

$ fsr serve shop/app --desktop
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.

Tutorial
an island
const paths = await openFile({
  filters: [{ name: "CSV", extensions: ["csv"] }],
});
if (paths) await actions.$root.importProducts({ path: paths[0] });
terminal

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.

03 · launchRun

The loader runs once, with the command line after -- as params. The root page is an Ink component.

04 · on each keyRespond

useInput steps the page's state and only the cells that changed are redrawn. No Node and no React run.

terminal

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.

Tutorial
app/routes/page.tsx
export default function Counter() {
  const [count, setCount] = useState(0);
  useInput((input) => {
    if (input === "+") setCount(count + 1);
  });
  return <Text bold>Count: {count}</Text>;
}
terminal

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.

Tutorial
an effect
useEffect(() => {
  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]);
Security

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 build runs build.rs and 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.

TypeScript
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 };
}
planIR
(let count (reduce (values (session cart)) 0 (fn (n q) (+ n q))))
Array methods such as reduce compile to IR nodes that the Rust server evaluates without a JavaScript engine.

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.
Best forLarge apps
SubsitesNative microfrontends
FrameworksReact, Vue, Svelte, Tera and web components
SSR or CSRAutomatic
Performance116,484 req/s
Server Attack SurfaceNo JS runtime
DeploymentOne binary
DesktopBuilt in, experimental
TerminalInk, with no Node, experimental