Skip to content
renkudocs

HTML first.
Behavior where
it belongs.

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.

Live client componentSee the code ↓

Live counter

Hello, renku.

0

Double: 0

Server HTML, ready for a local update.

Try the button and edit the name. The count, greeting and derived value update locally.

One authoring model. Two places to run.

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.

  1. 01 →

    Write

    Compose .tsrx templates, TypeScript logic and StyleX styles.

  2. 02 →

    Check & build

    TypeScript checks source. Bun produces Worker modules, CSS and browser entries.

  3. 03 →

    Serve

    The Worker renders HTML. Boundaries stream independently; static assets come from Cloudflare.

  4. 04 →

    Activate

    Complete client roots attach to existing DOM. State updates only dependent bindings.

Send what is useful. Let the rest arrive.

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.

This request, rendered on the server

GET /

This text is already in the response. It stays readable with JavaScript disabled.

Understand server components →
src/request-info.tsrx
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> };
}

Two results. Independent arrival.

Result A

Waiting for the server

The shell is already here. This region is still waiting.

Result B

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 →

Show the streaming source
src/streaming.tsrx
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.

A little state. Only the updates it needs.

The counter above starts as server HTML. When its module arrives, renku attaches behavior to those same nodes. Setup runs once per mounted lifetime.

Read, derive, respond.

state stores a value. derive computes from reactive reads. An event batches related writes, and the compiler turns template expressions into dependent DOM bindings.

Keep the user's input.

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 →
src/counter.tsrx
"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.

Make pages addressable. Share their structure.

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.

This site is the example.

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.

renku.config.ts
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" },
    ],
  }],
});

Write the structure. Compile the styling.

Use TSRX for every template and StyleX for every style. TypeScript checks authored props and expressions before Bun builds the Worker and browser modules.

This preview uses the same StyleX properties as the sample. The stylesheet is served as a hashed static asset; there is no separate StyleX dependency for your app.

Learn TSRX and StyleX →
src/panel.tsrx
import * as stylex from "renku/stylex";

const styles = stylex.create({
  panel: {
    padding: 24,
    color: "#047857",
    backgroundColor: "#edf7f2",
    borderRadius: 3,
  },
});

export default function Panel() @{
  <aside {...stylex.props(styles.panel)}>
    Styles are compiled into the generated stylesheet.
  </aside>
}

Start with the app you are looking at.

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.

Run it. Read it. Change 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.

Terminal
git clone https://github.com/antgrade/hill.git
cd hill
bun install --frozen-lockfile
bun run --cwd apps/docs dev

Working 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 →