Manual contentsGetting StartedBrowse 103 chapters
Manual 6 min read

Frontend Overview

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:

suprnova new my-app                       # Svelte 5 (default)
suprnova new my-app --frontend svelte     # Svelte 5
suprnova new my-app --frontend react      # React 19
suprnova new my-app --frontend vue        # Vue 3.5

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 suprnova::{InertiaProps, Request, Response, inertia_response};

#[derive(InertiaProps)]
pub struct HomeProps {
    pub title: String,
    pub message: String,
}

pub async fn index(req: Request) -> Response {
    inertia_response!(&req, "Home", HomeProps {
        title: "Welcome".into(),
        message: "Hello from Suprnova!".into(),
    })
}

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:

suprnova generate-types

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
App::inertia_share("appName", "Suprnova");
App::inertia_share("appVersion", env!("CARGO_PKG_VERSION"));

// Async / per-request shared data goes through the trait.
App::register_inertia_shared(Arc::new(AppSharedData));

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 suprnova::{InertiaResponse, FrameworkError, Request, Response};

pub async fn dashboard(req: Request) -> Response {
    let resp = InertiaResponse::new("Dashboard")
        .with("title", "Dashboard")
        .lazy("recent_orders", || async {
            Ok::<_, FrameworkError>(load_recent_orders().await?)
        })
        .defer("notifications", || async {
            Ok::<_, FrameworkError>(load_notifications().await?)
        })
        .resolve(&req)
        .await?;
    Ok(resp)
}

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 suprnova::{Inertia, InertiaConfig};

Inertia::install(&InertiaConfig::new().version(env!("CARGO_PKG_VERSION")))
    .expect("Inertia install failed");

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:

suprnova serve

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/:

cd frontend && npm run build
APP_ENV=production suprnova serve --backend-only

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 walks frontend/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-types reads 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.

Next