Lithair is a memory-first web framework I build in Rust — event-sourced store on disk, everything served from memory, one static binary. This post stands on its own: it's about what happened when I finally built a real product on top of it.
In stela's design file, one line stayed marked "unverified" for a month:
That Lithair's event store writes synchronously. If it buffers, the site rebuild replays a log that is missing the post you just wrote — and silently renders stale pages.
The whole architecture rests on that line. And you cannot settle it by reading code: you have to build the product, POST a post, POST a rebuild, and look whether the page is there.
It's there. It flushes. That's PR #2 of stela, and it's more or less the whole story of this post: what you learn about a framework the day you actually build something on it.
What it is
A stela is a standing stone bearing a public inscription — laws, poems, announcements. The blog of antiquity. stela is the modern one: a blog engine that runs as one binary.
stela new myblog
stela serve
No external database, no Node at runtime, no build tool beside it. Content lives in an event-sourced store on disk, served memory-first by Lithair. When you publish, the affected pages are rendered inside the binary (Tera + Markdown) and pushed straight into memory — no generator, no pipeline, no restart. A reader gets finished HTML; a page view never waits on a template engine.
A theme is a folder of Tera templates. Every administrative surface — the editor, the rebuild, the dashboard, the login — hangs off one random prefix per install, printed once at creation: a scanner walking its /wp-admin list finds nothing to authenticate against. And the Docker image is FROM scratch: the static binary and nothing else, so no shell and no distribution to track CVEs for.
It's also the idea I wrote down here in June — "a kind of WordPress, but in Lithair". It has a name now.
The product that pulls the framework
Here's what happened when I started writing this for real. Lithair was at v1.3.0. Five days later it was at v1.9.0.
Not out of ambition. Each version answers a gap stela had just hit:
stela needs… Lithair ships
──────────────────────────────────────────────────────────────────────
to react to a write and rebuild the page, v1.8.0 on_mutation — a native hook,
without a hand-rolled rebuild route same channel as the SSE stream
the login off /auth, the address every v1.8.0 with_auth_path — the route
scanner tries first moves wherever you want
a login that works from a browser v1.9.0 the login sets a session
(a cookie, not only a Bearer token) cookie, the guards accept it,
logout clears it
not to have a mutation replayed (post-1.9.0) cross-site requests on
from a third-party site a cookie session are rejected
The v1.8.0 release note says it plainly: "The consumer-driven release: everything here came from building a real site on Lithair." And on the default /auth, the same note calls it what it is — "an unauthenticated oracle for wordlists, the /wp-admin shape". Nobody writes that designing a framework in a vacuum. You write it right after seeing your own login page sitting exactly where a bot looks for it.
v1.9.0 is more instructive still. Making the login work from a browser exposed the session layer as "a pile of fragments: seven authorities for the cookie name, config knobs read but never applied, a cleanup task that never ran on the RBAC path". Seven places decided the cookie's name. It worked as long as nobody combined two paths. The first real consumer combined them.
It's the same pattern as elsewhere this summer: options that existed without being read. The note closes with "every session/cookie option now does what its documentation says". It took a product for that to become true.
What reading doesn't find
These things don't show up reviewing a framework. They show up when you use it for something with users, URLs and feeds.
-
A blog has no extensions in its URLs. Lithair's file server derives
Content-Typefrom the path extension./posts/my-posthas none — so every page would have gone out asapplication/octet-stream, and the browser would have downloaded it instead of rendering it. stela serves its pages through its own route, and the gap went upstream (lithair #193, closed in v1.4.0). -
A slug is untrusted input. It arrives through the API and is used to build a path. A post whose slug was
../../evilwrote outside/posts/and landed unescaped in the RSS feed. Found by running the server, not by reading it; now refused at the one point every rendered page funnels through. -
An RSS feed is XML, and URLs contain
/. Tera's XML escaping turns/into/: every link in the feed becamehttp://…— valid XML, useless link. -
A page and the feed share one namespace. A page whose slug is
rss.xmlwould replace the feed for every subscriber — or/index.htmlitself. The filter written against path traversal turns out to prevent this too, and a check now guards it so nobody ever "relaxes" it. -
delete_assetreported success without deleting anything. Filed on Lithair (#227) the following month. The same underlying defect as everywhere else: success reported while the work never happened.
None of those five are framework questions. They're blog questions. The framework couldn't ask them on its own.
Dogfooding, all the way down
stela is also the first project where the other three tools work together on something real. The rule is written into its repo: cidx owns code quality, probatum owns behaviour, and a check belongs to exactly one of them.
The first rendered page was written backwards: six probatum checks committed red, then turned green in the next commit.
✓ serve starts and answers (ready in 0.2s)
✓ publishing a post is accepted (HTTP 201)
✓ rebuild is accepted (HTTP 200)
✓ the published post is on the index (HTTP 200)
✓ the post has its own page, markdown rendered (HTTP 200)
✓ the feed lists it (HTTP 200)
And in the other direction, stela sends back what it finds: from this small repo, eleven issues and four PRs have been filed upstream on Lithair, cidx and probatum. Ten are closed. When the first cut of stela new was blocked by an isolation gap in cidx, the issue went there, cidx 3.3 fixed it, and the slice unblocked.
A product is the best bug report a toolchain can receive. It doesn't say "this should work"; it says "here is what I tried to do".
Where it honestly stands
stela's README carries the warning at the top, and I'll repeat it verbatim: it works, and it isn't shipped. You can scaffold a blog, sign in, write, publish. What isn't done is delivering it: 0.0.1 on crates.io is a name reservation, no image is published, no version is tagged. Today it's cargo build or docker build.
The last piece of the minimal scope — pages outside the stream, /about rather than /posts/about — sits in a PR still open. And I haven't touched it since late August; the agent that watches my repos counted zero commits this week, and it's right.
On the Lithair side, the cross-site protection has been on main for eighteen days without a tag. There's a v1.10.0 waiting on a decision, not on code — you know the tune by now.
And the destination: this very site. arcker.org runs today on a custom Lithair binary. It will migrate onto stela. That's the whole reason to build a product rather than one more site: the next time the framework is missing something, I'll know because my own blog will tell me.
French original on arcker.org.
Top comments (0)