VeloJS

Full-stack TypeScript.One mental model.

Build web apps where server and client share a codebase. Pages, APIs, webhooks, and real-time — one framework, one language, one component.

One component. SSR or SSG, same code.

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.

Dashboard.tsx
loaderLoads data from the server before the UI renders
SSRSSG
action_*Call server code from the UI like a local function
SSR
ComponentYour UI — rendered on the server, hydrated on the client
useLoader()action_*()
SSRSSG
server bundle
client bundle

Same component, same business rules

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.

Zero path mapping

Routes are generated automatically. Export a function, the framework registers it. No path mapping, no handler plumbing, nothing to wire by hand.

SSR or SSG, same code

Ship dynamic or pre-rendered with one flag. Same component, same loader, same TypeScript — the build mode doesn't change what you write.

What writing VeloJS feels like

A real component. The loader fetches, the action mutates, the Component renders — all in the same file, all in TypeScript.

app/Products.tsx
// 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>
    );
};

No fetch, no endpoints

The button calls action_toggleActiveProduct directly. The plugin wires the server call for you.

No duplicated types

The loader returns data. The Component consumes it. Same TypeScript, both sides.

No glue code

No request handlers, no zod schemas, no React Query. You write logic; velojs does the plumbing.

One routes.tsx. Every URL.

Pages, APIs, webhooks — all declared in a single tree. Middleware flows down; paths resolve automatically.

app/routes.tsx
// 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,
        },
    ],
}];
  • Rootlayout
    • /Homepage
    • /appAdminLayout🛡 authlayout
      • /dashboardDashboard🛡 authpage
      • /productsProducts🛡 authpage
    • /api/githubgithubWebhookPOST · endpoint
resolves to↓
GET/→Home
GET/app/dashboard→Dashboard🛡 auth
GET/app/products→Products🛡 auth
POST/api/github→githubWebhook

Paths compose automatically

Nested path segments stack up. /app + /dashboard becomes /app/dashboard.

Middleware flows down the branches

Set on a parent node, inherited by every descendant. Auth on /app protects every route under it.

One tree, all HTTP surfaces

Pages, APIs, webhooks — declared side by side. The framework discovers them by shape.

Real-time, when you need it.

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>;
};

Server pushes to the client

stream_* broadcasts events via SSE. The framework handles the transport; the browser reconnects automatically.

Two-way when it matters

socket_* opens a WebSocket. Client and server speak through the same typed primitive, in the same file.

Same middleware tree

Auth, rate limits — same inheritance as pages and actions. Real-time doesn't break your route structure.

Everything else, included.

No plugins to add, no escape hatches. The platform under the framework is already production-ready.

Hybrid SSR + SPA

Server-rendered first paint, client-side navigation after. No opt-in flag — it's how the framework works.

Static Generation

Same component, ship as static HTML. One CLI flag flips it: velojs build --static.

Reactive Signals

@preact/signals baked in. Fine-grained updates, no virtual DOM thrash, no re-render storms.

Type-Safe

Full TypeScript. Loaders, actions, middlewares — typed end to end. Autocomplete everywhere.

Zero Config

One plugin, one install. Hono + Preact + Vite + wouter wired together out of the box.

Testing Built In

createTestApp() spins up your app in memory. HTTP, streams, sockets — all from one API.

Get from zero to running in seconds.

bash
$npx @mauroandre/velojs init my-app