Getting started
Run the docs, then build a small app from complete files. Local setup requires access to the private repository; you can try the live examples without cloning.
On this page
Install Bun 1.4.4 and Node.js 26.12.1. From a terminal, clone the repository, install its pinned dependencies, and start the docs. Open http://127.0.0.1:3000. The native TypeScript 7.1 checker is supplied by the workspace; you do not need a global TypeScript install. Expected result: the docs home page loads locally.
renku is in development: it is a local workspace package, with no published release or project generator yet. Authenticate Git with an account that has repository access before cloning.
git clone https://github.com/antgrade/hill.git
cd hill
bun install --frozen-lockfile
bun run --cwd apps/docs devExplore apps/docs first, or create apps/my-app with this package.json. Run bun install from the repository root after adding the workspace. The following files form a complete small app; all paths are relative to apps/my-app.
{
"name": "my-app",
"private": true,
"type": "module",
"dependencies": { "renku": "workspace:*" },
"scripts": { "dev": "renku dev", "build": "renku build" }
}Extend the framework configuration to enable strict checking, Worker types and TSRX content mapping. Include your application source and route configuration.
{
"extends": "renku/tsconfig",
"include": ["src/**/*.ts", "src/**/*.tsrx", "renku.config.ts"]
}Save this configuration in your app's root. Paths to page and layout modules are relative to this file. With config-defined routes, renku generates the Worker entrypoint.
import { bindings, defineConfig } from "renku";
export default defineConfig({
worker: {
name: "my-app",
compatibilityDate: "2026-10-06",
env: { ASSETS: bindings.assets() },
},
routes: [{
layout: "./src/document.tsrx",
children: [
{ index: true, page: "./src/home.tsrx" },
{ path: "notes/:id", page: "./src/note.tsrx" },
],
}],
});The outer layout returns the full HTML document. The generated stylesheet and runtime URLs come from renku/assets; do not hardcode build hashes. Everything inside children becomes the matched page.
import type { LayoutProps } from "renku";
import { assets } from "renku/assets";
export default function Document({ children }: LayoutProps) @{
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My renku app</title>
<link rel="stylesheet" href={assets.stylesheet} />
<script src={assets.runtime} async />
</head>
<body>{children}</body>
</html>
}This synchronous component renders useful initial HTML on the server. Its module activates in the browser when the complete root is present. You can add StyleX after the behavior works.
"use client";
import { state, derive } from "renku/client";
export default function Counter({ initial }: { initial: number }) {
const count = state(initial);
const name = state("renku");
const doubled = derive(() => count.get() * 2);
return @{
<section>
<label>Name <input bindValue={name} /></label>
<p>Hello, {name.get()}.</p>
<button onClick={() => count.set(count.get() + 1)}>
Add one
</button>
<output>{count.get()}</output>
<p>Double: {doubled()}</p>
</section>
};
}The waits here only make streaming visible. Replace them with your application work. The second result can finish while the first one is still pending.
import { Boundary } from "renku";
async function Result({ seconds }: { seconds: number }) {
// A deliberate wait for this demo. Your data work goes here.
await new Promise(resolve => setTimeout(resolve, seconds * 1000));
return @{ <p>Ready after {seconds} seconds.</p> };
}
export default function Streaming() @{
<section>
<Boundary fallback={<p>Waiting for 5 seconds…</p>}
error={<p>This result could not load.</p>}>
<Result seconds={5} />
</Boundary>
<Boundary fallback={<p>Waiting for 2 seconds…</p>}
error={<p>This result could not load.</p>}>
<Result seconds={2} />
</Boundary>
</section>
}Server templates can import client components and pass JSON-shaped props. State, callbacks, and server context do not cross this boundary.
import Counter from "./counter.tsrx";
import Streaming from "./streaming.tsrx";
export default function Home() @{
<main>
<h1>My first renku app</h1>
<Counter initial={0} />
<Streaming />
<a href="/notes/hello">Read a note</a>
</main>
}From the repository root, run bun run --cwd apps/my-app dev. Open http://127.0.0.1:3000/notes/hello to see hello as the id. Stop the docs dev process first if it already uses port 3000. Saved changes rebuild and reload the whole document.
import type { PageProps } from "renku";
export default function Note({ params }: PageProps<"id">) @{
<article>
<h1>Note: {params.id}</h1>
<a href="/">Back home</a>
</article>
}Open http://127.0.0.1:3000/. The counter starts at 0; Add one changes it to 1 and Double to 2. Edit the name to update the greeting. The 2-second result appears before the 5-second result. Follow Read a note to /notes/hello, then run the build command from the repository root.
Expected result: the build completes and writes apps/my-app/.cloudflare/output/v0/. You now have server HTML, a client component, streamed content, and a typed route.
bun run --cwd apps/my-app buildA running app with an interactive counter, independent streamed results, and a parameterized note page.