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)

Atoms

An atom is the unit of state in Mimir. It is a small, frozen definition, a unique key, a default value, and a persistence flag, created with atom(config). It holds no value of its own; the store holds the live value, keyed by the atom's key.

Defining an atom

1
2
3
4
5
6
7
import { atom } from "ekko:rune/mimir";
 
export const themeAtom = atom({
key: "site-theme", // required, globally unique string
default: "dark", // required, the initial value
persist: true, // optional, default true
});

atom() validates and freezes the definition:

  • key must be a non-empty string. It is the identity of the state, used for storage, hydration, and

subscriptions.

  • default is required (passing no default throws). It is the value before anything sets the atom.
  • persist defaults to true. When a session is active, persist: true atoms are saved to IndexedDB and

restored on reload; persist: false atoms stay in memory only. (Without a session, persist has no effect, see Persistence & sessions.)

The returned object is frozen, you cannot mutate a definition. You change the value through the store (set, hooks), never the atom.

Keys are identity, choose them carefully

The key is how the value is stored, hydrated, and persisted. Two consequences:

  • Keys must be globally unique. Two atoms with the same key are the same slot in the store. Namespacing

helps: "cart:items", "ui:sidebar-open", "auth:user".

  • Keys are stable contracts. Renaming a key orphans any persisted value under the old name and any

server-seeded value addressed by the old name. Treat a key like a database column name.

1
2
3
export const cartAtom = atom({ key: "cart:items", default: [] });
export const sidebarAtom = atom({ key: "ui:sidebar-open", default: false });
export const userAtom = atom({ key: "auth:user", default: null });

Where atoms live

By convention, in atoms/, one module per concern. Import them where used:

1
2
3
// atoms/theme.ts
import { atom } from "ekko:rune/mimir";
export const themeAtom = atom({ key: "site-theme", default: "dark" });
1
2
3
// any component
import { useAtom } from "ekko:rune/mimir";
import { themeAtom } from "../atoms/theme";

No <Provider> wraps your app, atoms are module-level singletons backed by the single store. Importing the atom is all the "wiring" there is.

Default values and laziness

The store is lazy: an atom's value is materialised the first time it is read or written (_ensureAtom). Until then it logically holds its default. This means defining a thousand atoms costs nothing until they are touched.

default can be any serializable value, primitive, array, or object:

1
2
3
4
export const prefsAtom = atom({
key: "prefs",
default: { density: "comfortable", notifications: true },
});

Keep default serializable (no functions, class instances, Map/Set, circular refs). Mimir serializes atom values for SSR seeding and IndexedDB persistence; non-serializable defaults break those paths. See Pitfalls.

Typing atoms

atom<T>(config) infers T from default, or you can specify it for unions and nullable values:

1
2
3
4
type Theme = "dark" | "light";
export const themeAtom = atom<Theme>({ key: "site-theme", default: "dark" });
 
export const userAtom = atom<User | null>({ key: "auth:user", default: null });

useAtom(themeAtom) is then typed [Theme, (v: Theme | ((p: Theme) => Theme)) => void].

Atoms vs selectors

An atom is writable state. A selector is read-only derived state computed from other atoms (or selectors). You set atoms; you only get/read selectors. See Selectors.

Next: reading and writing atom values, Reading & writing.