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)

Persistence and sessions

Client navigation preserves atoms because they live outside the component tree. But a full reload (F5, direct URL, browser restart) rebuilds the whole page, in-memory atoms reset to their defaults. To survive that, Mimir can persist atoms to IndexedDB under a session. This is what makes a cart, a theme, or a draft outlast a refresh.

Sessions: none, ephemeral, domain

Persistence is off by default (session === "none"). Turn it on with mimir.session(mode):

1
2
3
4
5
import { mimir } from "ekko:rune/mimir";
 
mimir.session("ephemeral"); // persist for this browser tab/session
// or
mimir.session("domain"); // persist across tabs and restarts, for this origin
ModeScopeSurvives...Use for
none (default)in-memory onlyclient navigationtransient UI state you do not want to outlive a reload
ephemeralthis tab's sessionreloads within the tab (cleared when the tab closes)per-session drafts, a wizard's progress
domainthe whole originreloads, new tabs, browser restartstheme, cart, long-lived preferences

The server communicates the active mode to the client via __sessionMode in the hydration payload, so the client initialises the same session the app intends.

Persistence is client-side (IndexedDB), so verify it in a real browser. The server always renders an atom's default (or whatever ssr() seeds) — it has no IndexedDB. A curl of the SSR HTML therefore shows the default, not the persisted value. To confirm persistence works: open the app in a browser, change the state, then reload (F5) — the value should survive. To make a persisted value appear in the server-rendered HTML too, seed it from the server with ssr().__atoms (see SSR & hydration).

What gets persisted

Only atoms with persist: true (the default) are saved, and only when a session is active. An atom you mark persist: false stays in memory even under a session, useful for state that should reset on reload (a transient "is a modal open" flag) while other atoms persist.

1
2
export const cartAtom = atom({ key: "cart:items", default: [] }); // persisted (default)
export const modalAtom = atom({ key: "ui:modal", default: null, persist: false }); // never persisted

How it works (client)

Under a session, the client Mimir:

  1. On init, reads previously persisted values from IndexedDB (database mimir, store kv) and loads them

into the store, then applies the server seed on top per the seed modes.

  1. On every set/reset of a persist atom, marks the key dirty and schedules a debounced flush

(default ~100ms) that writes the dirty keys to IndexedDB. Debouncing coalesces rapid updates into one write.

  1. Tracks a session id; if the stored session does not match (a fresh session, or ephemeral in a new tab),

it starts clean, so stale state from a previous session does not leak in.

You do not manage any of this, you call session(...) once and read/write atoms normally.

When to start the session

Call mimir.session(...) early on the client, typically in your root layout (or a small bridge component it renders), so persistence is active before the first user interaction:

1
2
3
4
5
6
7
8
// a tiny client component rendered by the root layout
import { useEffect } from "@ekko/react";
import { mimir } from "ekko:rune/mimir";
 
export default function SessionBridge() {
useEffect(() => { mimir.session("domain"); }, []);
return null;
}

(The server's __sessionMode and the client's session(...) should agree; setting it in the layout that renders on both keeps them consistent.)

Interaction with SSR seeding

On reload under a session, the client merges three sources, in this order of precedence:

  1. Server force/merge seeds (__force overwrites, __merge deep-merges) , the server is authoritative

where it says so.

  1. Persisted IndexedDB values , what the user left behind.
  2. Plain server seeds and atom defaults , fill only where nothing else set the value.

So a domain session restores the user's cart on reload, while a __force seed (say, a server-validated cart after checkout) still wins when the server insists. Design your seed modes with this precedence in mind.

Clearing a session

1
mimir.clearSession(); // drop persisted state, reset persisted atoms to defaults, mode -> "none"

Use it on logout or "reset everything". It clears IndexedDB and resets atoms to their defaults, notifying subscribers so the UI updates.

Privacy and size

IndexedDB persistence stores whatever you put in persist atoms on the user's device. Keep it to UI state and non-sensitive data; do not persist secrets or large blobs. Mark anything sensitive persist: false.

Next: putting it together, Patterns.