Suprnova bridges Rust handlers to a single-page frontend via Inertia.js 3.4.0. You write controllers in Rust and pages in Svelte, React, or Vue; the framework moves typed props between them without a separate HTTP API in the middle.
Three first-class starters
suprnova new <name> scaffolds a working project. The --frontend flag
picks the SPA layer:
All three scaffolds share the same stack:
| Layer | Version |
|---|---|
| Inertia client adapter | @inertiajs/{svelte,react,vue3} 3.4.0 |
| Build tool | Vite 8 |
| Styling | Tailwind v4 (@tailwindcss/vite) |
| TypeScript | strict mode |
The choice is per-project. There is no "primary" framework on the
server side - inertia_response! resolves whichever extension your
chosen scaffold uses (.svelte, .tsx, .vue), and App::inertia_share,
partial reloads, and TypeScript prop generation all behave identically
across the three.
Architecture
Browser
+-------------------------------------------------+
| SPA (Svelte / React / Vue) |
| +---------------+ +---------------+ |
| | Home.svelte | | Users/Show.tsx| ... |
| +-------+-------+ +-------+-------+ |
| | typed props from Rust struct |
| +-------v-------------------------------+ |
| | Inertia client adapter | |
+---+------------------+------------------+--+----+
|
| HTTP (JSON on XHR, HTML on first load)
v
+-------------------------------------------------+
| Suprnova server |
| +------------------------------------------+ |
| | Controllers / handlers | |
| | inertia_response!(&req, "Home", | |
| | HomeProps { ... }) | |
| +------------------------------------------+ |
+-------------------------------------------------+
The first request returns an HTML shell with the initial page object
embedded in the mount node's data-page attribute. Subsequent visits
go through <Link> / router.visit, send X-Inertia: true, and get
back a JSON page object - the adapter swaps the component without a
full reload.
A complete page round-trip
The controller defines its props as a Rust struct, derives
InertiaProps, and hands the value to the inertia_response! macro:
use ;
pub async
A few things the macro does for you. First, it validates at compile
time that the page component file actually exists under
frontend/src/pages/Home.{svelte,tsx,jsx,vue} - typos surface as a
build error, not a 404 in the browser. Second, it serializes the
HomeProps struct, unfolds it into one prop per top-level key so
partial reloads can filter, and resolves any lazy or deferred props
against &req before returning. The macro evaluates to a
Result<HttpResponse, FrameworkError>, which the Response return type
accepts directly.
The matching Svelte page (the default scaffold):
<!-- frontend/src/pages/Home.svelte -->
<script lang="ts">
import type { HomeProps } from '../types/inertia-props'
let { title, message }: HomeProps = $props()
</script>
<div class="font-sans p-8 max-w-xl mx-auto">
<h1 class="text-3xl font-bold">{title}</h1>
<p class="mt-2">{message}</p>
</div>
For the React and Vue equivalents see Page Components.
Generating TypeScript types
Every #[derive(InertiaProps)] struct in your src/ becomes a
TypeScript interface in frontend/src/types/inertia-props.ts:
Pass --routes and the same command also emits
frontend/src/types/routes.ts - type-safe URL + method pairs scraped
from your routes! macro that work directly with Inertia v2+ APIs. The full type-mapping table and
route-helper shape live in TypeScript Types.
Shared data
Anything that should appear on every page (the authenticated user, the current locale, app metadata) is registered once at boot and merged into every Inertia response:
// In bootstrap.rs
inertia_share;
inertia_share;
// Async / per-request shared data goes through the trait.
register_inertia_shared;
Three flavours, in order of precedence (later wins at the same key):
| API | When the value materializes |
|---|---|
App::inertia_share(k, v) |
Sync, set once at boot |
App::inertia_share_lazy(k, || async { ... }) |
Per response, recomputed |
App::inertia_share_once(k, || async { ... }) |
Per response, then client-cached |
App::register_inertia_shared(Arc::new(impl)) |
Per request, sees &req |
Per-page props attached on the response builder always overwrite shared data at the same key.
Partial reloads and lazy props
The same InertiaResponse builder exposes Inertia v3's full prop
toolkit - eager, lazy, optional, deferred, merge, once - and Suprnova
honors the v3 partial-reload headers (X-Inertia-Partial-Data,
X-Inertia-Partial-Except, X-Inertia-Reset,
X-Inertia-Except-Once-Props) automatically. The example below
attaches three props with different evaluation rules:
use ;
pub async
inertia_response! covers the eager-props case; everything past that
goes through the builder. The full surface - optional, merge,
once, scroll, flash, paginate, SSR, version mismatch, history
encryption - is documented in
Inertia Responses.
Bootstrap
A scaffolded app installs the two protocol-critical middlewares in one
call inside bootstrap.rs:
use ;
install
.expect;
install returns Result - it fails closed if InertiaConfig resolves to
production mode (the default under APP_ENV=production) but no Vite
manifest can be found, rather than silently falling back to a legacy
asset path. See Development vs production
below.
That registers InertiaVersionMiddleware (emits 409 + X-Inertia-Location
on asset-version mismatch so stale clients reload) and Inertia303Middleware
(rewrites 302 → 303 on non-GET Inertia visits so the follow-up is
unambiguously a GET). Both used to be opt-in; Inertia::install makes
them the default.
Development vs production
In development, the Vite dev server runs alongside the backend and serves HMR-enabled assets:
This boots the Rust server and vite together. The HTML shell loads
modules from http://localhost:5765.
For production, build the frontend once and point the backend at the
hashed manifest under public/assets/:
&&
APP_ENV=production
InertiaConfig::default() derives production vs. development mode from
APP_ENV (via Environment::detect().is_production()) - APP_ENV=production
is what makes the HTML shell load built assets instead of the Vite dev
server. Inertia::install then fails boot loudly if it can't find a
manifest to back that decision, rather than silently falling back to a
stale hardcoded path.
Suprnova reads public/assets/.vite/manifest.json to resolve hashed
entry points plus any transitive imports for modulepreload. SSR is
optional - opt in by pointing InertiaConfig::ssr(...) at a running
@inertiajs/{vue3,react,svelte}/server worker.
Why Suprnova diverges
Three intentional departures from how a typical Inertia setup looks elsewhere:
- Compile-time component validation. The
inertia_response!macro walksfrontend/src/pages/at build time and refuses to expand if the component file is missing, suggesting the closest match. You cannot ship a controller that points at a deleted page. - Typed props as the source of truth. Page props are Rust structs
with
#[derive(InertiaProps)].suprnova generate-typesreads them and writes TypeScript interfaces - the frontend types are derived from the backend, not maintained in parallel. - Svelte as the default. Inertia's documentation reaches for Vue and React first; the Suprnova scaffolder defaults to Svelte 5 (runes-on). React 19 and Vue 3.5 are first-class, not afterthoughts - same protocol, same prop pipeline, same generator output.
