Result A
Waiting for the server
The shell is already here. This region is still waiting.
A full-stack framework for Cloudflare. Build applications for people and agents with server HTML, independent streaming, and explicit reactive client components.
In development. The examples on this page run on renku today.
Hello, .
Double:
Try the button and edit the name. The count, greeting and derived value update locally.
The counter above arrives as server HTML: its name, greeting and count are already in the response. A "use client" module attaches browser behavior to that same markup. Change the name or add one: the affected bindings update locally. You choose that boundary.
Compose .tsrx templates, TypeScript logic and StyleX styles.
TypeScript checks source. Bun produces Worker modules, CSS and browser entries.
The Worker renders HTML. Boundaries stream independently; static assets come from Cloudflare.
Complete client roots attach to existing DOM. State updates only dependent bindings.
The document and its navigation do not need to wait for every result. Server components receive request-local context; boundaries give deferred work a loading and error state.
GET /
This text is already in the response. It stays readable with JavaScript disabled.
Understand server components →import type { RenderContext } from "renku";
export default function RequestInfo(_props: {}, context: RenderContext) {
const path = new URL(context.request.url).pathname;
return @{ <p>The Worker received {context.request.method} {path}.</p> };
}Waiting for the server
The shell is already here. This region is still waiting.
Waiting for the server
The shell is already here. This region is still waiting.
These regions use real server boundaries. To make the sequence visible, the timed demo waits 2 and 5 seconds. Watch it stream →
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>
}The waits are for the demo. Put your asynchronous data work inside the result component.
The counter above starts as server HTML. When its module arrives, renku attaches behavior to those same nodes. Setup runs once per mounted lifetime.
state stores a value. derive computes from reactive reads. An event batches related writes, and the compiler turns template expressions into dependent DOM bindings.
bindValue adopts edits made before activation without replaying events. Keyed lists preserve row identity; removing a branch cleans up its listeners and owned work.
Explore client components →"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 behavior behind the live counter above, with presentation styles omitted.
Declare a route tree, wrap it in a document layout, and read typed path parameters. Only the matched page and layout modules are imported on the server.
Follow a guide, reload it, then use Back. These are normal document links, and they work without JavaScript.
Routing↗Layouts↗The shared shell wraps every guide. Layouts reuse structure; they do not retain browser state between navigations.
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" },
],
}],
});Use Bun 1.4.4 and Node.js 26.12.1. renku is currently a workspace package in this repository; there is no published npm release or project generator yet. Local setup requires access to the private repository; the live examples here work without it.
Open http://127.0.0.1:3000, edit a template in apps/docs/src, and save. The development server rebuilds and reloads the document.
The getting-started guide has complete configuration, document, page and client-component files. Then build with renku build and deploy its generated output with cf deploy --prebuilt.
git clone https://github.com/antgrade/hill.git
cd hill
bun install --frozen-lockfile
bun run --cwd apps/docs devWorking today: server rendering, streaming, synchronous client components, config-defined routes, TSRX, StyleX and Cloudflare builds. Local-first SQLite replicas, durable offline edits and sync are available in the offline showcase. Async client effects and retained routing remain future design work. Read the current guides →
Received from the server
An independent region, delivered through a server boundary.
Received from the server
An independent region, delivered through a server boundary.