Orisha static tiers: bake what is hot, stream the rest from disk
The static server had one gear: embed. Every file under root was read at compile time, gzip-baked into a complete HTTP response, and shipped inside the binary — for korulang.org that meant a 429 MB generated Zig file and a binary whose coldest pages occupied the same rodata as its hottest. This week the declaration learned a second gear.
One JSON body, five new keys
The orisha:static declaration already took its config as a JSON object — the same shape koru/fly:config speaks. The tier keys ride inside it, no new syntax:
orisha:static(site) {
"root": "../../../korulang_org/build",
"fallback": "200.html",
"gzip_only": true,
"disk": ["docs/**", "book/**", "archives/**"],
"bake": ["_app/**", "*.html", "*.css", "*.js"],
"embed_max": 1048576,
"dates": "orisha-dates.json",
"disk_before": "2026-07-01"
} Every file under root is classified once, at transform time, before any read — which is the point: disk files never enter the binary at all.
disk— glob patterns that never embed.*and?stay inside a path segment;**crosses directories.bake— when present, a whitelist: anything it does not match demotes to disk. Absent, everything not otherwise demoted bakes, exactly as before.embed_max— a byte ceiling. Over it, the file goes to disk. This retires the old silent 10 MB skip: the cap used to produce a hole (no route at all), now it produces a tier.dates+disk_before— a manifest the site build emits, mapping root-relative paths toYYYY-MM-DD; paths older than the absolute cutoff demote. Absolute, not “older than N days” — a relative threshold would make the binary depend on the build date.
Precedence is fixed: disk wins outright, then the bake whitelist, then dates, then size. Lookup order at runtime is baked map → disk map → fallback → 404, and the disk table is only emitted when tiers exist — a declaration with none of the new keys generates byte-identical output to before.
Disk is not second-class
A disk route is still a compile-time route: the path was enumerated while the program compiled, so there is no directory scanning and no new traversal surface. The generated disk_by_path map resolves the URL to a root-relative file, and serveStaticFile serves it from STATIC_DIR through the same (mtime, size) cache orisha:serve-file already used — one stat per request, body cached forever after.
Parity came with it, because a tier that serves worse than bake is a demotion users can feel: strong ETags computed per representation with If-None-Match → 304, the same Cache-Control/CDN-Cache-Control/Vercel-CDN-Cache-Control policy baked routes carry (immutable under /_app/), .br/.gz sibling negotiation, and HEAD via a new Response.head_only that renders the would-be Content-Length with an empty body iovec. orisha:serve-file now delegates to the same function, so the arena’s /static/:file route inherited all of it.
Orisha owns the codec
The obvious question was compression: gzip_only bakes gzipped bytes, but disk files would have gone out identity unless the site build emitted .gz siblings. The answer was to stop asking the build. With no sibling on disk, serveStaticFile gzips the cached body once per file per process at default level and keeps the variant under its own cache key — its own ETag, its own Vary: Accept-Encoding, shared freshness with the identity file. Sibling files still win when present (better ratio, zero first-touch CPU), which makes a framework precompress step an optimization rather than a requirement. Measured on a real post: 31,125 bytes became 8,296 on the wire, and If-None-Match against the gzip ETag answered 304.
The dates manifest is the site’s job
Recency is not expressible as a path glob — blog slugs carry no dates, and file mtimes are meaningless because every rebuild touches everything. The contract that fell out: the build emits orisha-dates.json ({ "blog/<slug>.html": "2026-07-01" }) from the frontmatter it already parses; the declaration says disk_before. Orisha learns nothing about SvelteKit; the site owns the data, the declaration owns the policy. korulang_org’s build now emits it — 201 posts mapped, drafts and unbuilt pages excluded.
What it measured
Against the real korulang_org/build, locally: 12,848 baked / 354 disk routes with a conservative config — generated Zig 394 MB vs the 429 MB all-bake baseline, compile 51 s in Debug, RSS 1.7 MB after light traffic because untouched pages never fault in and disk files never enter RAM at all. Baked and disk latency were indistinguishable at localhost scale (~0.4 ms). The honest caveat: the −8 % is what conservative globs buy; the HTML bulk is still baked, and disk-ing blog/** wholesale is a product call, not a default. And disk files must actually ship — STATIC_DIR has to point at them in the image, which is a deploy concern the declaration can’t solve alone.
The tier model also happens to be the shape profile-guided optimization wanted anyway: orisha.profile already orders routes by measured hits, and the same data could eventually drive the tier itself — hot pages bake, the cold tail streams. Hand-written globs are the bootstrap; the log is the endgame.