DEV Community

arcker
arcker

Posted on Originally published at arcker.org

stela — six Lithair releases in five days, because a product demanded them

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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-Type from the path extension. /posts/my-post has none — so every page would have gone out as application/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 ../../evil wrote 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 became http://… — valid XML, useless link.
  • A page and the feed share one namespace. A page whose slug is rss.xml would replace the feed for every subscriber — or /index.html itself. The filter written against path traversal turns out to prevent this too, and a check now guards it so nobody ever "relaxes" it.
  • delete_asset reported 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)
Enter fullscreen mode Exit fullscreen mode

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)