← the tour · concept

concept i

The one loop

A user acts; the store changes; every watcher re-reads it and re-renders. Everything else is a refinement of this.

orion has exactly one round-trip, and the whole framework is shaped to serve it. A user acts. A command validates the request and writes to the store, then publishes a topic that says only “something here changed.” Every live view subscribed to that topic re-reads the store and re-renders its region; the server streams the fresh HTML over SSE and the browser morphs it in place. No client cache, no diff protocol, no second copy of the truth.

act
POST /todos
command
validate · write
bus
publish("todos")
live
store → Html
fat morph
SSE · brotli
↑  the store — SQLite in WAL mode — is the only source of truth  ↑

CQRS, the Datastar way

Reads and writes are different route kinds with different types — and keeping them apart is what makes the loop hold:

The read: a pure function, run forever

A view is a function of the store, nothing else. There is no client state to drift, no optimistic update to reconcile — the server re-renders the whole region and the browser morphs the difference. Data lives in the feature, as named SQL:

// features/todos/todos.view.ts — the read side
import { html, type Html } from "../../belt/html.ts"  // belt is vendored in your repo

export function TodoList(todos: Todo[]): Html {
  const remaining = todos.filter((t) => !t.done).length
  return html`
    <section id="todos">
      <p role="status">${remaining} of ${todos.length} remaining</p>
      <ul>${todos.map(TodoItem)}</ul>
    </section>`
}

The write: validate, mutate, publish

A command answers the requester with only requester-scoped feedback — a form reset, a validation error, a toast. The shared change it made flows back to every open page (the requester's included) through their live streams. That discipline is the whole reason single-player and multiplayer are the same code.

// features/todos/todos.commands.ts — the write side
import { command } from "../../belt/http.ts"
import { parse } from "../../belt/shape.ts"
import { formErrors } from "../../belt/ds.ts"
import { toast } from "../patterns/toast.ts"   // a vendored pattern, also yours

export const add = command(async (ctx, respond) => {
  const r = await parse(todoShape, ctx.signals)
  if (!r.ok) return respond.patch(formErrors("todo", todoShape, r.issues))
  todos.insert(ctx.store, r.value.title)
  ctx.bus.publish("todos")            // → every watcher re-renders
  toast(respond, "Added", { tone: "success" })
})

The load-bearing trick: events carry no state. A published topic just means “re-read the store.” Because every render is the complete current truth, a dropped, coalesced, or out-of-order render can't leave the screen wrong — the next one heals it, and every viewer is consistent by construction.

Why this scales down to one person

There's no WebSocket protocol to design, no client store to keep in sync, no reconciliation layer. The work you'd normally spend wiring real-time goes away, because real-time is just the read side doing its ordinary job. This shape — server-rendered HTML, morphed over SSE, one SQLite node as the source of truth — is the one Anders Murphy's Hyperlith proves out; orion carries it into a Node toolbelt.

orion ✦ a belt of stars · built on datastar