Build web apps where server and client share a codebase. Pages, APIs, webhooks, and real-time — one framework, one language, one component.
Loader, action, and UI live in the same file. The plugin splits server from client at build time; the CLI picks SSR or SSG. Business logic travels with the UI that uses it.
Data fetching, mutations, and UI live side by side in the same file. Business rules stay where they belong — next to the UI that uses them.
Routes are generated automatically. Export a function, the framework registers it. No path mapping, no handler plumbing, nothing to wire by hand.
Ship dynamic or pre-rendered with one flag. Same component, same loader, same TypeScript — the build mode doesn't change what you write.
A real component. The loader fetches, the action mutates, the Component renders — all in the same file, all in TypeScript.
// app/Products.tsx
// Runs on the server — fetches the products list
export const loader = async () => ({
products: await db.products.findMany(),
});
// Toggle a product's active state on the server
export const action_toggleActiveProduct = async ({ body }) => {
await db.products.toggleActive(body.id);
};
export const Component = () => {
// Reads the products from the loader
const { data } = useLoader();
// Calls the server action for the given product id
const toggle = (id) => action_toggleActiveProduct({ body: { id } });
return (
<ul>
{data.value?.products.map(p => (
<li key={p.id}>
{p.name}
<button onClick={() => toggle(p.id)}>Toggle</button>
</li>
))}
</ul>
);
};The button calls action_toggleActiveProduct directly. The plugin wires the server call for you.
The loader returns data. The Component consumes it. Same TypeScript, both sides.
No request handlers, no zod schemas, no React Query. You write logic; velojs does the plumbing.
Pages, APIs, webhooks — all declared in a single tree. Middleware flows down; paths resolve automatically.
// app/routes.tsx
export default [{
module: Root,
isRoot: true,
children: [
{ path: "/", module: Home },
{
path: "/app",
middlewares: [authMiddleware],
children: [
{ path: "/dashboard", module: Dashboard },
{ path: "/products", module: Products },
],
},
// Endpoint, not a page — handles GitHub webhooks
{
path: "/api/github",
method: "POST",
handler: githubWebhook,
},
],
}];| GET | / | → | Home | |
| GET | /app/dashboard | → | Dashboard | 🛡 auth |
| GET | /app/products | → | Products | 🛡 auth |
| POST | /api/github | → | githubWebhook |
Nested path segments stack up. /app + /dashboard becomes /app/dashboard.
Set on a parent node, inherited by every descendant. Auth on /app protects every route under it.
Pages, APIs, webhooks — declared side by side. The framework discovers them by shape.
Push events with stream_*. Open a bidirectional connection with socket_*. Same shape as loader and action — declare, export, done.
// app/Deploy.tsx
// Server pushes events through this stream
export const stream_progress = createEventStream<string>();
export const action_deploy = async () => {
stream_progress.emit("Build complete");
stream_progress.emit("Deploy live");
};
export const Component = () => {
// Subscribes — updates reactively as events arrive
const { data } = useEventStream(stream_progress);
return <p>{data.value ?? "Waiting..."}</p>;
};// app/Chat.tsx
// Bidirectional — both sides send and receive
export const socket_chat = async ({ incoming, send, keepOpen }) => {
send({ from: "server", text: "Hi!" });
keepOpen();
// Server receives client messages, replies
for await (const msg of incoming) {
send({ from: "server", text: "Got: " + msg.text });
}
};
export const Component = () => {
// Receive via onMessage; send via the returned function
const { send } = useSocket(socket_chat, {
onMessage: (msg) => console.log(msg),
});
return <button onClick={() => send({ text: "Hi!" })}>Say Hi</button>;
};stream_* broadcasts events via SSE. The framework handles the transport; the browser reconnects automatically.
socket_* opens a WebSocket. Client and server speak through the same typed primitive, in the same file.
Auth, rate limits — same inheritance as pages and actions. Real-time doesn't break your route structure.
No plugins to add, no escape hatches. The platform under the framework is already production-ready.
Server-rendered first paint, client-side navigation after. No opt-in flag — it's how the framework works.
Same component, ship as static HTML. One CLI flag flips it: velojs build --static.
@preact/signals baked in. Fine-grained updates, no virtual DOM thrash, no re-render storms.
Full TypeScript. Loaders, actions, middlewares — typed end to end. Autocomplete everywhere.
One plugin, one install. Hono + Preact + Vite + wouter wired together out of the box.
createTestApp() spins up your app in memory. HTTP, streams, sockets — all from one API.