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)

The dev loop

Day-to-day, you edit code and see the result. The recommended loop is a single command.

Start here: ekko dev

1
2
ekko dev # watches files, rebuilds the client bundle, HMR-reloads the browser
ekko dev --port 4000 # override the port

ekko dev watches the filesystem, rebuilds the client bundle when browser code changes, and pushes an HMR reload to open tabs over a WebSocket. It replaces the build, run, and restart dance with one process. The default entry is server.tsx.

By default it runs with full trust (it is a dev tool, so permissions are not enforced). Real enforcement is ekko run's job in production; ekko dev is full trust unless you opt into enforcement.

To discover exactly which permissions your app needs before shipping, pass --allow. That flips ekko dev into deny-by-default, granting only what you list, so a missing capability surfaces during development:

1
ekko dev --allow=fs,net # deny-by-default: only fs and net are granted

The mental model: two compilations

When you reach for the manual loop, it helps to know that rune has two distinct compilation paths: the server code (transpiled and executed by the runtime as it runs) and the client bundle (built ahead of time by ekko build --client).

You changed...What to do
A page's server render or ssr() onlyRestart ekko run server.tsx.
Anything that runs in the browser (component JSX, event handlers, client logic)ekko build --client, then restart the server so it serves the new bundle.
SCSS (styles/global.scss)Restart the server, SCSS is compiled at startup.
Markdown docs (content/docs-src/*.md)ekko run _build/build-docs.ts --allow=fs to regenerate content/docs/docs.data.ts, then ekko build --client + restart.
server.tsx, routes, configRestart the server.

Because the page component is rendered both on the server (for SSR) and in the browser (after hydration), most visible changes need the client rebuild and a restart.

Manual / CI fallback

When you are not watching (a CI build, a production image, or a one-off run), drive the two compilations yourself instead of ekko dev:

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

This is the explicit build-then-run path, with real permission enforcement. Use it for non-watch and production builds; use ekko dev for interactive development.

Live reload (HMR)

When the client bundle changes, rune can tell open browsers to reload. The server exposes a tiny WebSocket at /__ekko_hmr and watches .ekko/build/client/__hmr (a token file the build updates). When the token changes, connected pages receive a reload message and refresh. This gives you "save, rebuild, the tab refreshes itself" without a manual F5, the page still does a full reload (it is not React Fast Refresh), but it is automatic.

The flow:

  1. You run the build watcher (or re-run ekko build --client).
  2. The build writes a new token into .ekko/build/client/__hmr.
  3. The running server notices the token changed and pushes reload to every open tab.

If you prefer, ignore HMR and just restart, the result is identical.

A practical setup

For most work, one terminal running ekko dev is enough. If you prefer the manual loop, use two terminals:

1
2
3
4
5
# Terminal 1 , the server (restart when server code or SCSS changes)
ekko run server.tsx --allow=fs,net,env
 
# Terminal 2 , rebuild the client on demand (or wire a watcher)
ekko build --client

For docs-heavy projects, regenerate the embedded docs data when you touch markdown:

1
2
3
ekko run _build/build-docs.ts --allow=fs # markdown -> content/docs/docs.data.ts
ekko build --client # re-bundle (the embedded docs data changed)
# restart the server (or just keep ekko dev running, which rebuilds for you)

What "it built" looks like

A successful client build prints the chunks it wrote and the manifest path:

Building client bundles (N entries + hydrate)...
  → 84 chunks to .ekko/build/client/
  → manifest: .ekko/build/manifest.json

The server, on start, prints the listening URL and (for eager SSR) how many pages it pre-rendered:

SSR cache: 48 pages rendered (eager)
EkkoJS SSR listening on http://0.0.0.0:3000

Ports

The example apps read a PORT environment variable (requires --allow=env) and fall back to 3000:

1
2
const PORT = Number(Ekko?.env?.get?.("PORT")) || 3000;
const app = createApp({ port: PORT, /* ... */ });

Run two rune apps side by side by giving them different ports:

1
PORT=3001 ekko run server.tsx --allow=fs,net,env

You now have the loop. Next, build something real end to end in the Tutorial, or jump to Core Concepts for the model behind it all.