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)

Subscriptions

The hooks (useAtom, useAtomValue) are built on a lower-level primitive: subscriptions. When you need to react to state changes outside a React component, persist on change, log, sync to another system, you subscribe to the store directly.

mimir.subscribe(atomOrSelector, fn)

Registers a callback that runs whenever the atom (or selector) changes. Returns an unsubscribe function.

1
2
3
4
5
6
7
8
9
10
import { mimir } from "ekko:rune/mimir";
import { themeAtom } from "../atoms/theme";
 
const unsubscribe = mimir.subscribe(themeAtom, (value) => {
console.log("theme is now", value);
document.documentElement.classList.toggle("dark", value === "dark");
});
 
// later, when you no longer care:
unsubscribe();

The callback receives the new value. It fires on every change (after set/reset actually changes the value, no-op writes do not fire it).

Subscribing to a selector

You can subscribe to derived state too. Mimir primes the selector's dependency set on subscribe, so even if the selector was never read before, a later change to one of its (transitive) dependency atoms notifies the subscriber:

1
const stop = mimir.subscribe(cartTotal, (total) => updateBadge(total));

The imperative store API

mimir is the store instance. Outside React you have the full surface:

1
2
3
4
5
mimir.get(atomOrSelector) // read current value (computes selectors)
mimir.set(atom, valueOrUpdater) // write; notifies subscribers + dependent selectors
mimir.reset(atom) // set back to default
mimir.subscribe(a, fn) // → unsubscribe()
mimir.snapshot() // a plain object of every key → value (for debugging/export)

set accepts a value or an updater (prev) => next, and short-circuits when the value is unchanged (Object.is).

When to use subscriptions

  • Bridging to non-React code , update the <html class> for theming, sync an atom to the URL, push a

value to a Web Worker.

  • Side-effects on change , analytics, telemetry, autosave.
  • Imperative reads in handlers , read the latest value in an event handler without making the component

subscribe.

Inside components, prefer the hooks, they handle subscribe/unsubscribe with the component lifecycle for you. Reach for subscribe when there is no component to hang the lifecycle on.

Lifecycle and cleanup

Always call the returned unsubscribe when the subscription's owner goes away, otherwise the callback (and anything it closes over) leaks. In a React effect:

1
2
3
4
useEffect(() => {
const off = mimir.subscribe(themeAtom, applyThemeToDom);
return off; // cleanup on unmount
}, []);

(That is essentially what useAtomValue does internally, this is the manual version for cases the hook does not cover.)

How the hooks use it

For context, the client useAtom is roughly:

1
2
3
4
5
6
7
8
function useAtom(a) {
const [val, setVal] = useState(() => mimir.get(a));
useEffect(() => {
setVal(mimir.get(a)); // sync to current on mount
return mimir.subscribe(a, v => setVal(v)); // re-render on change; cleanup on unmount
}, [a.key]);
return [val, useCallback(v => mimir.set(a, v), [a])];
}

So "a component re-renders when an atom changes" is just a subscription whose callback calls setVal. The store is the source of truth; React is one of its subscribers.

Next: the server/client handoff, SSR & hydration.