← the tour · reference
a guided read
Reading an orion codebase
No framework magic to learn first. Here is a whole orion app — a todo list with accounts, multiplayer, and RBAC — read in the order you'd actually explore it: the map, the entry point, routing, a feature, then a seam. Every snippet below is real code from the example app, lightly trimmed.
An orion app is made of two kinds of thing, and once you can tell them apart the whole layout reads itself. Slices are features — a folder per product capability, owning its own tables, reads, and writes. Seams are the shared machinery a slice leans on — auth, the journal, routing, the store — each a small, swappable surface you own. Slices are where your product lives; seams are the belt holding them together.
0The map
Open the repo and this is the shape. The annotations are the whole mental model:
example/
├─ main.ts // YOU own the entry point — wire pieces, start a server
├─ paths.ts // every URL in one typed registry (no string literals in handlers)
├─ config.ts // the app's settings shape — validated at boot
│
├─ features/ // ← SLICES: one folder per product capability
│ └─ todos/
│ ├─ todos.sql.ts // data — migrations + named SQL (owns its tables)
│ ├─ todos.view.ts // read — pure functions: store → HTML
│ ├─ todos.commands.ts // write — validate, mutate, publish
│ └─ todos.routes.ts // manifest — the one resource the app mounts
│
├─ lining/ // ← SEAMS: shared machinery features lean on
│ ├─ auth/ // sessions, RBAC, masquerade — the access seam
│ └─ journal/ // append-only audit log — the history seam
│
└─ ../belt/ // ← the toolbelt, VENDORED into your repo (zero deps)
├─ http.ts · region.ts · store.ts · bus.ts // the core seams
└─ authz.ts · jobs.ts · canon.ts · … // batteries, each behind a seam
Two rules explain the layout. A slice owns its tables —
nothing outside features/todos/ writes the todos tables, so a feature is a unit
you can read, change, or delete in one place. The belt is yours — it's
copied into the repo, not installed from npm, so every seam above is greppable, patchable
code, not a hidden dependency. Why vendoring →
1main.ts — the entry point
Start here, because orion does. There is no app container, no lifecycle framework, no dependency injection — you construct the pieces and wire them. The whole boot is one readable file; here is its spine, in order:
// 0. config first — validated before anything else exists, so a missing
// or malformed setting fails the boot HERE, naming the field.
const config = await loadConfig(appConfig)
const log = createLog(config.logLevel, { app: "example" })
// 1. the three things every request needs: a store, a bus, a logger.
const store = sqliteStore(config.databasePath) // synchronous SQLite — the source of truth
const bus = createBus() // in-process pub/sub — "something changed"
const deps = { store, bus, log, config }
// 2. features are mounted EXPLICITLY. Adding a feature = one line here.
const features = [auth, todos, admin, reports, billing, webhooks, journal]
for (const f of features) store.migrate(f.name, f.migrations) // each slice owns its schema
// 3. one router; mount each feature's routes onto it.
const router = createRouter({ store, bus, log, dev: config.dev })
for (const f of features) router.mount(f.routes)
for (const f of features) f.init?.(deps) // WIRE bus subs & caches — nothing runs yet
// 4. start the server. router.handle is fetch-shaped: (Request) => Response,
// so the same function runs on node:http, Bun, Deno, or a Worker.
const server = serve(router.handle).listen(config.port)
// 5. NOW run background work (pollers, queue consumers) — after the server
// is accepting traffic, so anything a feature kicks off has somewhere to land.
const stops = await startFeatures(features, deps)
gracefulShutdown({ server, store, log, stops })
Read the comment numbers as a lifecycle: migrate (schema), mount (URLs exist), init (wires connect, but nothing fires), listen (traffic flows), start (background work runs). The split between init and start is deliberate — wiring is cheap and ordered; side effects wait until there's a running server to catch them.
The load-bearing idea: the framework's job ends at giving you good
pieces. main.ts is ordinary code you can read top to bottom — no
inversion of control, no "where does this get called?" There is exactly one place the app is
assembled, and you are looking at it.
2Routing — a feature is one resource
Every URL the app answers is declared in one typed registry, paths.ts, so a
handler never writes a URL string and a renamed route fails to compile instead of 404-ing in
production:
// paths.ts — the URL map. One source of truth for every link, form, and fetch.
export const paths = {
todos: {
base: route("/todos"),
item: route("/todos/:id"), // .href({ id }) builds it, type-checked
confirmDelete: route("/todos/:id/confirm-delete"),
},
}
A feature exposes its URLs as a single resource — the REST verbs plus named
commands for non-CRUD intent. This is the slice's public face; main.ts mounts it
without knowing anything about the inside:
// todos.routes.ts — the manifest. Everything the app needs to mount the feature.
import { resource, view } from "../../belt/http.ts"
export const todos = {
name: "todos",
migrations: sql.migrations, // the slice's schema travels with it
routes: resource(paths.todos.base.pattern, {
index: view((ctx) => TodoPage(sql.all(ctx.store))), // GET /todos — full page
live: todoStream, // GET /todos/live — the read stream
create: commands.create, // POST /todos
update: commands.update, // PATCH /todos/:id
destroy: commands.destroy, // DELETE /todos/:id
commands: { "clear-done": commands.clearDone }, // POST /todos/commands/clear-done
}),
}
Notice what routing isn't: there's no middleware stack, no decorator
soup, no file-system convention guessing your intent. A route is a verb, a path, and a
function. index renders a full page (works before JS loads — hypermedia first);
live is the SSE stream that morphs over it; the writes are plain commands.
3A slice — the four files
Inside features/todos/, the four files map exactly onto the one loop: data,
read, write, manifest. Read them in that order.
Data — todos.sql.ts
Migrations and named queries. No ORM, no query builder; the only opinions are where this lives (in the feature) and what shape it takes (functions over the store). Migrations are append-only and forward — a schema change is a new entry, never an edit:
import type { Store } from "../../belt/store.ts"
export const migrations = [
`create table todos (
id integer primary key,
title text not null,
done integer not null default 0
)`,
`alter table todos add column created_by integer`, // appended when RBAC landed
]
// named SQL — plain functions over the Store. This is the data API.
export const all = (store: Store) => store.query<Todo>("select id, title, done from todos order by id")
export const insert = (store: Store, title: string, by: number | null) =>
store.exec("insert into todos (title, created_by) values (?, ?)", title, by)
Read — todos.view.ts
A view is a pure function of the data. Every render is the complete current state of the region — there is no diffing to get wrong; the browser morphs the difference:
import { html, type Html } from "../../belt/html.ts"
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((t) => html`<li>${t.title}</li>`)}</ul>
</section>`
}
Write — todos.commands.ts
A command validates, mutates, and publishes. It answers the requester with only requester-scoped feedback (a validation error, a toast); the shared change it made flows back to every open page through their live streams:
import { command } from "../../belt/http.ts"
import { parse } from "../../belt/shape.ts"
import { formErrors } from "../../belt/ds.ts"
export const create = command(async (ctx, respond) => {
const r = await parse(todoShape, ctx.signals)
if (!r.ok) return respond.patch(formErrors("todo", todoShape, r.issues)) // just for you
sql.insert(ctx.store, r.value.title, currentUser(ctx)?.id ?? null)
ctx.bus.publish("todos") // → every watcher re-renders, including you
})
Manifest — todos.routes.ts
You already saw it (§2): the file that ties the other three together into one
resource and exports it for main.ts to mount. That's the whole
slice — four files, one capability, owning its own data end to end.
4Authorization — reading a seam
Now read a seam, to feel the difference. Auth lives in lining/auth/ —
same four-file shape as a slice, but it exists to be used by other slices. The whole
access surface is one watchable object built once:
// lining/auth/auth.guard.ts — the access seam, constructed once and shared.
import { createAuthz } from "../../belt/authz.ts"
export const authz = createAuthz(sql.grantsFor) // load a subject's grants (cached)
// authz.can(store, subjectId, action, resource) → boolean
A slice calls authz.can(...) wherever it renders or enforces. Because the answer
is just data, it folds straight into a view model — so the SAME render produces different DOM
per viewer. Here's the todos slice deciding, per row, who sees a delete button:
// todos.routes.ts — per-viewer affordances. Each render asks: may THIS user delete THIS todo?
function canDelete(ctx: Ctx): (todo: Todo) => boolean {
const user = currentUser(ctx)
return (todo) => !!user && authz.can(ctx.store, user.id, "delete", `todos:${todo.id}`)
}
// …and the live stream WATCHES authz, so a grant change repaints affected viewers:
live: region(
{ topics: ["todos"], watch: [authz], model: (ctx) => ({ items: sql.all(ctx.store), can: canDelete(ctx) }) },
(m) => TodoList(m.items, m.can),
)
Grant a viewer the delete role mid-session and their delete
controls appear — no refresh. authz is watchable, so the permission
change repaints every affected region. The enforcement point is still the command (the button
is a courtesy); the same watchable mechanism powers live feature flags and billing
entitlements. more on capabilities →
5Slices vs seams — the whole mental model
That's an orion app. Two kinds of thing, and you can now place any file in the repo:
Slices · features/
One folder per capability. Owns its tables, its reads, its writes. Self-contained enough
to read, change, or delete in one place. Adding one is a folder plus a line in
main.ts.
Seams · lining/ & belt/
Auth, journal, store, bus, router — the surfaces slices lean on. Small, watchable, and yours (vendored), so each is swappable without touching a single feature.
When you sit down to add a feature, the question is always the same: which seams does this slice need, and what are its four files? The framework doesn't answer it for you — it just makes the answer small.