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
- Pages & dynamic segments — file → URL mapping,
[id],[...slug], query params - Layouts & route groups —
layout.tsx, error boundaries,(group)/directories - Render modes —
staticvsssrvsisr, when to use each - Loaders & meta — server-side data fetch +
<head>injection - Navigation —
Link,useNavigate, prefetch on hover - Islands — hydrating only the interactive bits, JS-free pages
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.