Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Quick start

A five-minute tour: a page, a layout, some state, and client navigation. The Tutorial builds a complete app step by step; this page is the shortest path to "it works".

The fastest, correct way to start a rune app is ekko init rune. It writes the right ekko.json ("type": "rune", entry server.tsx, the React imports map) and a working starter, so you never hand-write the manifest:

1
2
3
4
5
ekko init rune my-app -t asgard-minimal # clean themed starter (recommended). Also: minimal (plain) | showcase, asgard (full demos to learn from)
cd my-app
ekko add # install declared deps (@ekko/react, @ekko/react-dom) + write ekko.lock
ekko build --client # generate the client bundles (.ekko/build)
ekko dev server.tsx # watch + HMR (or: ekko run server.tsx --allow=fs,net,env)

The rest of this page shows what those files contain, so you can edit them with confidence or build one by hand.

1. A page

Create pages/index.tsx. A page is a module with a default export (the React component) and an optional ssr() function that runs on the server.

1
2
3
4
5
6
7
8
9
10
11
12
13
// pages/index.tsx
export function ssr() {
return { title: "Home, My App" }; // sets <title>; can also seed state + head tags
}
 
export default function Home() {
return (
<main>
<h1>Hello rune</h1>
<p>This was server-rendered, then hydrated.</p>
</main>
);
}

2. A layout

pages/layout.tsx is the shell wrapped around every page. It receives children.

1
2
3
4
5
6
7
8
9
10
// pages/layout.tsx
export default function RootLayout({ children }: { children: any }) {
return (
<div className="app">
<header><a href="/">My App</a> · <a href="/about">About</a></header>
<main>{children}</main>
<footer>© 2026</footer>
</div>
);
}

3. Wire it up in server.tsx

server.tsx is the entry point. It registers the React renderer, builds the app, maps routes to page modules, and starts listening.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// server.tsx
import "@ekko/react";
import "@ekko/react-dom/server";
import { createApp, scanRoutes, readManifest } from "ekko:rune";
 
import RootLayout from "./pages/layout";
import * as Home from "./pages/index";
import * as About from "./pages/about";
 
const app = createApp({
port: 3000,
manifest: readManifest(),
layouts: { "": { layouts: [{ render: RootLayout }] } },
ssr: "eager",
});
 
const modules: any = { "/": Home, "/about": About };
for (const r of scanRoutes("pages")) {
const mod = modules[r.pattern];
if (mod) app.page(r.pattern, mod, { page: r.pageKey });
}
 
app.start();

scanRoutes("pages") discovers your files and returns { pattern, pageKey, ... } for each. You map each pattern to the imported module and call app.page. (Larger apps automate this; the tutorial shows the full pattern, including docs route generation.)

Why extensionless? Ekko owns module resolution. It sees a bare specifier, detects the source kind, transpiles .ts/.tsx, caches the converted JS, then resolves it, uniformly, decoupled from any bundler. Writing the extension would tie your code to one on-disk form and bypass that pipeline, so ekko run rejects it. Always import without an extension: ./pages/layout, not ./pages/layout.tsx.

4. Run it

1
2
ekko build --client
ekko run server.tsx --allow=fs,net,env

Visit http://localhost:3000. View source, you will see real HTML for the page, not an empty <div id="root">.

5. Add client-side navigation

Use the router's <Link> (or useRouter().navigate) for in-app links so navigation stays on the client:

1
2
3
4
5
import { Link } from "ekko:rune/router";
 
export default function Home() {
return <p>Go to <Link href="/about">About</Link>, no reload.</p>;
}

Never use window.location.href or a plain <a href> for in-app navigation, that triggers a full reload and discards client state. External links are fine as plain <a>. See Navigation.

6. Add some state

Define an atom once, use it anywhere. It persists across navigation.

1
2
3
// atoms/counter.ts
import { atom } from "ekko:rune/mimir";
export const countAtom = atom({ key: "count", default: 0 });
1
2
3
4
5
6
7
8
// any component
import { useAtom } from "ekko:rune/mimir";
import { countAtom } from "../atoms/counter";
 
export default function Counter() {
const [count, setCount] = useAtom(countAtom);
return <button onClick={() => setCount(c => c + 1)}>Clicked {count}×</button>;
}

Navigate away and back, the count is still there. That is the point of Mimir. Read the full story in Mimir.


That is the whole loop: pages with ssr(), a layout, server.tsx to wire them, the router for navigation, and atoms for state. Next, understand the folders: Project structure.