← the tour · concept
concept · auth
Authentication, and the split that runs through everything
Who you are is a session and a hashed token; what you may do is the next page. But auth is also where orion's organizing idea was found — the belt ships mechanism, you own policy — and that split now shapes the whole toolbelt.
Authentication is the question who is this? — distinct from authorization (may they do X?), which comes after. orion answers it the way the rest of the belt answers everything: with the dangerous, easy-to-get-wrong mechanism shipped as zero-dependency primitives, and the policy — what a user is, how long a session lasts, what your login page says — emitted as a feature slice you own and edit. The security model follows pilcrow's Copenhagen Book (auth.pilcrowonpaper.com), the same source that informed Lucia — a quietly load-bearing influence on this project.
Users and sessions — the model
Two tables, both yours, both plain SQL in the scaffolded slice. The only opinionated part is the shape of a session, and it's opinionated for a reason:
// lining/auth/auth.sql.ts — what `orion gen feature auth` emits. You own it.
import type { Store } from "../../belt/store.ts"
export const migrations = [
`create table users (
id integer primary key,
username text not null unique,
password_hash text not null, -- scrypt; never a plaintext password
created_at text not null default (datetime('now'))
)`,
`create table sessions (
id text primary key, -- = sha256(cookie token), NOT the token
user_id integer not null references users(id) on delete cascade,
expires_at integer not null -- unix seconds
)`,
]
The load-bearing line is the comment: the cookie holds a random
token; the database stores only its SHA-256 hash. So a leaked sessions
table can't be replayed — there's nothing in it an attacker can put in a cookie. This is the
pilcrow session model, and it costs you one sha256 per request to verify.
The split: mechanism in the belt, policy in your slice
Issuing a session touches things that are genuinely dangerous to hand-roll — token entropy,
constant-time comparison, cookie flags, password hashing parameters. Those live in
belt/auth.ts as primitives. Deciding things — how long a session lasts,
what counts as a user, what your login page says, whether the first account is an admin — is
policy, and it lives in the slice the generator emits.
belt/auth.ts
Mechanism — zero-dep, node:crypto
generateSessionToken, hashSessionToken,
sessionCookie / readCookie, checkOrigin, scrypt
password hash + verify (parameters encoded into the hash so they can be raised later), a
rate limiter. The parts you must not get wrong.
lining/auth/
Policy — a slice you own
The users/sessions tables, login/signup commands, currentUser, session TTL
and sliding refresh, first-account-is-admin bootstrap. Emitted by
orion gen feature auth as editable code. No Auth class, no
provider interface, no middleware.
// issuing a session — MECHANISM from the belt, POLICY (TTL, table) is yours
import { generateSessionToken, hashSessionToken, sessionCookie } from "../../belt/auth.ts"
const token = generateSessionToken() // 32 random bytes, base64url
sql.createSession(store, hashSessionToken(token), user.id, expiresAt) // store the HASH
// login/signup are raw handlers (a cookie means setting headers before the
// Response exists), so the Set-Cookie goes straight on the returned Response:
headers.set("set-cookie", sessionCookie("orion_session", token, { maxAge: SESSION_TTL_S }))
// reading it back — the cookie's token is sha256'd to find the row (auth.guard.ts)
export function currentUser(ctx: Ctx): User | undefined {
const token = readCookie(ctx.req, SESSION_COOKIE)
if (!token) return undefined
const row = sql.sessionWithUser(ctx.store, hashSessionToken(token))
if (!row || row.expiresAt < now) return undefined
// sliding expiry (pilcrow): past the half-life, extend — active users stay in
return row.user
}
"Bring your own auth" means simply not running the generator — point
currentUser at your IdP and keep the rest. There's no framework seam to satisfy,
because auth was never a framework object here; it's primitives plus a slice. Passwords use
scrypt from node:crypto — pilcrow's recommended choice when argon2
would mean a native dependency, and in keeping with orion's zero-dep stance.
Why this split runs through the whole belt
Mechanism-versus-policy didn't start as a manifesto — it came out of building auth, where the line between "must be correct" and "is your call" is unusually sharp. Once it was named, it turned out to describe how every good seam in the belt is drawn. The belt owns the mechanism; your code owns the meaning:
| Seam | Belt ships (mechanism) | You own (policy) |
|---|---|---|
| auth | tokens, hashing, cookies, origin checks | what a user is, session TTL, login UX, first-admin |
| jobs | atomic claim, retry, backoff, dead-letter | what the job actually does |
| tenancy | a store bound to an opaque key, leak-proof | what a tenant is — org, workspace, team |
| canon | the request lifecycle + structural fields | enrichers — tenant, plan, the flags evaluated |
| analytics | the append-only sink + buffered track() | the funnels and cohorts — your SQL questions |
It's the same test every time: if getting it wrong is a security or
correctness hole, the belt does it once, for everyone. If getting it wrong is just a different
product, it's yours. That's why there are no Auth / Tenant /
Job base classes to subclass — the belt hands you the sharp parts pre-sharpened
and stays out of the decisions.