DISCLAIMER: Expressed views on this blog are my own.
The vision for SnapFire FSR is still the same and stronger than ever: Replace Node.js. Today marks an escalation.
When I released FSR in September it did one thing. You wrote React and TypeScript, the build turned it into data and a Rust binary served the website with no Node in the request path.
That is still true. In the weeks since, FSR has changed where the TSX runs and what it can be written in. The same pages now run as a website, a desktop app or a terminal program. They can be written in React, Vue, Svelte or htmx. And if you have a Next.js app, fsr migrate moves it over.
Your TSX runs in three places
This is a counter written against Ink, the library Claude Code, Gemini CLI and Shopify CLI draw their terminals with:
import { useState } from "react";
import { Box, Text, useInput } from "@snapfire/fsr/terminal/ink";
export default function Counter() {
const [count, setCount] = useState(0);
useInput((input, key) => {
if (input === "+" || key.upArrow) setCount(count + 1);
});
return <Box borderStyle="round"><Text>{count}</Text></Box>;
}
On Node that needs Node, React and Yoga. FSR lowers it like any other component and runs it inside the Rust binary: taffy does the flexbox layout, a keypress steps the state and only the cells that changed are redrawn. There is no JavaScript engine in the process.
cargo install snapfire_fsr_cli --features terminal
fsr new my-tool --terminal
fsr dev my-tool/app
The terminal tutorial builds a terminal app step by step.
The desktop goes the other way. Your website opens in the operating system's own webview with the Rust host in the same process. Electron ships a Chromium and a Node runtime inside every app. This ships neither. The pages, loaders and actions are the ones the website serves.
cargo install snapfire_fsr_cli --features desktop
fsr dev my-app/app --desktop
Both are experimental. Desktop has only run on macOS so far and there is no packaging yet.
Write it in whatever you want
The release was React only. Now one page holds a React island, a Vue island, a Svelte island and an htmx region, sharing one store:
import { Island } from "@snapfire/fsr/client/react";
import { ReactCounter } from "@src/ui/ReactCounter";
import VueCounter from "@src/ui/VueCounter.vue";
import SvelteCounter from "@src/ui/SvelteCounter.svelte";
export default function Page() {
return (
<>
<Island when="visible"><ReactCounter /></Island>
<Island when="visible"><VueCounter /></Island>
<Island when="visible"><SvelteCounter /></Island>
</>
);
}
fsr use vue
fsr use svelte
cargo install snapfire_vue snapfire_svelte
Vue and Svelte render on the server in Rust, the same way React does. That is the part I care about most. The build runs Vue's and Svelte's own compilers in QuickJS, lowers what they produce and renders it on the server. Then the markup is checked byte for byte against what vue/server-renderer and svelte/server write. If FSR's output differs from the framework's own, that is a bug in FSR.
React 19 is supported alongside 18.
No JavaScript on the server
When I wrote the first post, removing Node was mostly about deploys: one binary, no node_modules, no idle interpreter. Since then I've come to think security is the bigger reason. Several of the worst Node incidents of the last few years belong to bug classes that need JavaScript running on the server:
- Deserializing into a JavaScript runtime. React2Shell made the RSC decoder run code without sign-in. FSR decodes a request into Rust values against the action's typed contract.
- Prototype pollution. A
__proto__key in a request body writes to every object in the process. The IR has no prototypes. - eval and template injection. The IR is a fixed set of operations written at build time. A request cannot add one.
- npm install scripts. ua-parser-js and Shai-Hulud ran on the machines that installed them.
fsr addvendors packages for the browser and runs no script. - Bugs in the runtime itself. V8, llhttp, undici and zlib are not in the process.
- Regular expression DoS in routing. Routes match through a radix tree with no regex.
The host also sets the defaults you would otherwise configure. It sends a Content Security Policy with a nonce per document, checks every write against its origin and gives the session a new id at sign-in. Sign-in supports OpenID Connect and passkeys.
Your authorization, your own regexes, the HTTP layer and the browser are still yours to handle.
Also new
- Images and fonts resized and served under hashed, immutable URLs
- Tailwind v4 ported to Rust
- MDX pages
- Share cards drawn in Rust from
opengraph-image.tsx - Sitemaps and PWA support
- Much more of the language lowers: loops, classes, try/catch, regex, Intl and Math that matches V8 bit for bit. A component that still does not lower becomes a browser island instead of failing the build.
fsr doctorchecks a deployment before it goes out; the host has health probes and a graceful drainfsr testruns specs, typechecks and runs your Rust tests, still without Node
Bring your Next.js app
cargo install snapfire_fsr_cli
fsr migrate ./my-next-app ./my-app
fsr dev my-app/app
It reads the App Router or the Pages Router and writes a new FSR project. Data fetching becomes loaders, server actions become FSR actions with typed inputs, generateMetadata becomes loader meta, next/image becomes Picture and NEXT_PUBLIC_ variables become public config. Prisma models become records and their calls a typed service.
It tells you when it cannot be moved, with the file and the line. It migrates Ink apps the same way. SvelteKit and Nuxt are next.
Or start fresh:
fsr new my-app
fsr dev my-app/app
Where to go next
- snapfirers.com is the SnapFire website.
- Learn has the tutorials: a working cart, form validation, sharing state through the store and a terminal app.
- The guide covers every part of FSR chapter by chapter.
- The docs are the reference.
- Compare puts FSR beside Next.js, Remix, SvelteKit and the rest.
- The code is on GitHub and the CLI is snapfire_fsr_cli on crates.io.
Desktop and terminal are early. If something does not lower, file an issue with the code.
No one really cares whether TypeScript or JavaScript is what runs. They care about what the code means to do. That's why I believe lowering made the most sense.