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)

Mimir pitfalls

The mistakes that actually bite, each with the symptom and the fix. If a Mimir-related bug is confusing you, it is almost certainly one of these.

1. Mutating a value in place

Symptom: you set an array/object but nothing re-renders.

Cause: Mimir compares the new value to the old with Object.is. If you mutate the existing object and return the same reference, it looks unchanged, so no notification.

1
2
3
4
setCart(items => { items.push(x); return items; }); // ✗ same reference, no update
setCart(items => [...items, x]); // ✓ new array
setPrefs(p => { p.density = "compact"; return p; }); // ✗
setPrefs(p => ({ ...p, density: "compact" })); // ✓

Fix: always produce a new value.

2. Duplicate or colliding keys

Symptom: two unrelated pieces of state interfere; setting one changes the other.

Cause: two atoms share a key. The key is the identity, same key = same store slot.

Fix: make keys globally unique and namespaced ("cart:items", "ui:sidebar-open"). Treat keys like database column names.

3. Renaming a key

Symptom: persisted state or server-seeded state "disappears" after a refactor.

Cause: the value is stored under the old key (in IndexedDB) and seeded under the old key (in __atoms). Renaming orphans both.

Fix: keep keys stable. If you must rename, migrate persisted data, or accept that existing users reset once.

4. Non-serializable values

Symptom: SSR seeding or persistence throws, or values silently vanish on reload.

Cause: atom values are serialized for __atoms (SSR) and IndexedDB (persistence). Functions, class instances, Map/Set, RegExp, BigInt, NaN/Infinity, and circular references do not survive.

Fix: store plain JSON-shaped data, primitives, arrays, plain objects. Reconstruct rich objects from plain data where you use them.

5. Seeding per-user data into a cached static page

Symptom: users see another user's data on first load.

Cause: a static route's HTML is cached and shared. Per-user values in its ssr().__atoms get baked into the shared HTML.

Fix: seed only non-personal defaults into cached pages and personalise on the client; or use a dynamic/shell route for per-user content. See Mimir → SSR & hydration.

6. Reading browser globals during render

Symptom: hydration mismatch warnings, a flash of wrong content.

Cause: reading window/localStorage/Date.now() during render produces different output on the server (where they do not exist) than on the client.

Fix: render a stable value first and update in an effect, or seed the value through an atom in ssr(). For theme specifically, also use the no-FOUC script. See SSR → hydration.

7. Using useState for state that should persist

Symptom: state resets every time the user navigates or refreshes.

Cause: useState lives in the component tree, which is torn down on navigation/reload.

Fix: if it should survive a navigation or refresh, put it in an atom (with a session for refresh). Keep useState for genuinely local, disposable state.

8. Forgetting to unsubscribe

Symptom: a slow memory leak, or callbacks firing for unmounted things.

Cause: mimir.subscribe(...) returns an unsubscribe function you did not call.

Fix: store and call it. In React effects, return the unsubscribe so React cleans it up on unmount. (The hooks already do this; only manual subscriptions need care.)

9. Accidental selector cycles

Symptom: Mimir: circular selector dependency at '...'.

Cause: selector A reads selector B which (transitively) reads A.

Fix: break the loop, derive both from a shared atom instead of from each other.

10. Expecting useSetAtom to re-render

Symptom: a component using useSetAtom does not update when the atom changes elsewhere.

Cause: useSetAtom is write-only; it deliberately does not subscribe (that is its performance benefit).

Fix: if the component also needs to read and react, use useAtom or useAtomValue.


That completes the Mimir chapter. From here: the UI scaffolding, Pages & Layouts, the server surface, API Routes, or build it all in the Tutorial.