Overview

Voltro's file-based router — pages, layouts, render modes, loaders, navigation, islands. The web side of the framework.

The web side of a Voltro app uses file-based routing: drop a TSX file under src/pages/, the CLI discovers it on every boot + save, and the file becomes a route. No router config, no manual <Route> declarations, no codegen step.

This section covers everything about how URLs map to React + how Voltro decides when to render, what to ship to the browser, and how to navigate between pages.

The shape of it

src/pages/
├── layout.tsx              # outer shell (wraps every page)
├── error.tsx               # error boundary
├── loading.tsx             # pending UI
├── not-found.tsx           # 404 fallback
├── index.tsx               # /
├── about.tsx               # /about
├── (marketing)/            # route group — no URL segment
│   ├── layout.tsx          # marketing-scoped layout
│   └── pricing.tsx         # /pricing
├── users/
│   ├── layout.tsx          # users-scoped layout
│   ├── error.tsx           # users-scoped error boundary
│   ├── [id].tsx            # /users/:id
│   └── index.tsx           # /users
└── docs/
    └── [...slug].tsx       # /docs/<anything>  (catch-all)

That's the whole router. No <Route>, no <Switch>, no useRoutes.

What's in this section

Architecture in one paragraph

The framework generates a .framework/app.tsx on every boot that imports each *.tsx file under src/pages/ (excluding node_modules), wraps them in their layout chains, and produces a <Router routes={…} /> element. A mount(App, { group }) call in .framework/main.tsx mounts it via react-dom/client's createRoot (or hydrateRoot for SSR pages). The router watches window.location + intercepts <Link> clicks for client-side nav.

You don't write any of this. The CLI regenerates it on every save in dev; the build pipeline freezes it for production.

Embedding the runtime in a foreign host

To mount Voltro's reactive runtime inside an app that owns its own routing (a Next.js / Remix / existing React shell), skip the generated boot and use the embeddable provider from the light @voltro/web/runtime subpath:

import { VoltroRuntimeProvider, type MountedApi } from '@voltro/web/runtime'

const apis: ReadonlyArray<MountedApi> = [
  { name: 'app', group, descriptors, wsUrl: 'wss://your-api.example.com/ws' },
]

export const Providers = ({ children }: { children: React.ReactNode }) => (
  <VoltroRuntimeProvider apis={apis}>{children}</VoltroRuntimeProvider>
)

Inside it, every @voltro/client hook (useSubscription, useMutation, useAction, …) resolves exactly as in a native Voltro app — it builds + supervises the per-api RpcClient-over-WebSocket + runtime + subscription cache and handles reconnect. children render immediately (a pending api reads undefined until connected — no spinner gate), so a prerendered host hydrates without a mismatch. The @voltro/web/runtime subpath pulls only the rpc/socket/runtime graph, not the router/mount/SSR — keeping the host bundle small. (FrameworkBoot, the framework's own web boot, is a thin wrapper over this provider.) Consume the framework as the published package — a file: link to an unbuilt checkout resolves the raw src export, which a foreign bundler can't handle.

Conventions

Pattern Behaviour
index.tsx in a directory Maps to the directory's URL (no segment for the filename).
[name].tsx Dynamic single segment. Available via useParams<{ name: string }>().
[...rest].tsx Catch-all. useParams<{ rest: string }>() joins the captured path with /.
[[...rest]].tsx Optional catch-all. Matches both /foo and /foo/bar/baz.
(name)/ Route group. Strips the segment from the URL but layouts inside still apply.
_*.tsx Private — discovery skips it. Useful for helpers next to pages.
*.island.tsx Hydration island. Bundles as a separate chunk; hydrated only when imported.

When NOT to use file-based routing

The pattern works for ~99% of apps. Edge cases:

  • Programmatically generated routes — when you can't know the URL ahead of time. Use [...slug].tsx + match inside.
  • i18n with URL-segment locale — see URL strategies for the shipped URL-prefix approach (/de/docs/foo).
  • Cross-tenant subdomains — handled at the reverse-proxy layer; the file-based router serves one host's paths.

For everything else, drop a file + done.

Where to next — building the UI inside the page

Routing gets you to a page; the Schema-driven UI section fills it. It's the other half of the frontend story:

  • Forms & tables<AutoForm> binds to a mutation, <DataTable> to a query; fields + columns come from the descriptors' effect/Schema, validation + live auto-optimistic for free.
  • Reactive components — drop-in workflow progress, presence/multiplayer, and AI chat over the durable backend.
  • Client utilities — the bound-hook toolbox beyond useSubscription/useMutation: useCan, useDerived, usePreview, useUndo, useProvenance, useAsyncValidation, useOutbox, useWindowedSubscription, and more.