Tutorial — build your first JORVEL app
Start to finish in ~20 minutes: a host shell that loads a federated dashboard remote, a file-based route, server data, a session gate, and a deploy. Assumes Node 20+ and a package manager (pnpm recommended).
1 · Scaffold a workspace
npm create jorvel@latest my-shop
cd my-shop
pnpm installInteractive prompts pick a template, package manager, and Tailwind. You get apps/, libs/, jorvel.config.json, CI workflows, ESLint, Vitest, and Git hooks — ready to run.
2 · Generate a host + a remote
jorvel generate host shell --port 3000
jorvel generate remote dashboard --port 3001
jorvel federation # wires the host's remotes map to the dashboardThe host owns top-level URLs; the remote owns its sub-paths. jorvel federation reads each app's jorvel.app.json and writes the jorvel.federation.json the runtime consumes.
3 · Run the dev server
jorvel dev --proxy-remotes --hmr-remotesOpen localhost:3000. --proxy-remotes serves the remote on the host origin (so CSP/cookies behave like prod); --hmr-remotes reloads the host when the remote recompiles.
4 · Add a route
Drop a file in the remote's src/pages/ — file-based, Next-style:
export default function Order({ params }: { params: { id: string } }) {
return <main><h2>Order {params.id}</h2></main>;
}jorvel routes --watch # regenerates src/jorvel.routes.tsThe host now matches /dashboard/orders/42.
5 · Load data
Client-side with useQuery (cache + stale-while-revalidate), or server-side with defineLoader for hydration-ready reads.
import { useQuery } from '@jorvel/runtime';
function Orders() {
const { data, isLoading } = useQuery({
queryKey: ['orders'],
queryFn: () => fetch('/api/orders').then((r) => r.json()),
});
if (isLoading) return <p>Loading…</p>;
return <ul>{data.map((o) => <li key={o.id}>{o.total}</li>)}</ul>;
}Need a database? jorvel add db scaffolds Drizzle (guide).
6 · Gate a route
Add a middleware that redirects unauthenticated users:
import { defineMiddleware, redirect, next } from '@jorvel/runtime';
import { getSession } from '@jorvel/security';
export default defineMiddleware(async (ctx) => {
if (!ctx.pathname.startsWith('/dashboard')) return next();
const user = await getSession(ctx.request ?? '', { secret: process.env.SESSION_SECRET! });
return user ? next() : redirect('/login?from=' + encodeURIComponent(ctx.pathname));
});7 · Mutate with a form
import { Form, defineAction } from '@jorvel/runtime';
const createOrder = defineAction(async (fd: FormData) =>
fetch('/api/orders', { method: 'POST', body: fd }).then((r) => r.json()),
);
function NewOrder({ csrfToken }: { csrfToken: string }) {
return (
<Form action={createOrder} csrf={{ token: csrfToken }}>
{({ pending }) => (<><input name="sku" /><button disabled={pending}>Create</button></>)}
</Form>
);
}8 · Build & deploy
jorvel build # all apps
jorvel federation diff --base main # confirm no breaking contract changes
jorvel deploy --target vercel # or cloudflare | node | docker | bun | deno | netlify | github-pagesYou shipped a micro-frontend
