Three things landed together: Stario 4.1, Ox, and Orbit. They are not a suite. They are three answers to three jobs I kept hitting — how an endpoint is named, how the browser stays honest, and how an app reaches a server I control.
Stario 4.1 — Route
Stario 4 made the URL module config: a UrlPath is a location. Templates call .href(). Handlers register on that same value. That part stays.
What 4.0 left split was the method. You still wrote app.get(HOME, home) in one file and at.get(HOME.href()) in another. The path was shared. The verb was typed twice.
Route is one HTTP method on one UrlPath. You declare the endpoint once, register it with app.add, and emit the Datastar fetch from the same object:
from stario import App, Routefrom stario.datastar import at, data SEND = Route.post("/rooms/{room_id}/send") app.add(SEND, send)# in a view:h.Button(data.on("click", at.fetch(SEND, {"room_id": room.id})), "Send")The variable is the resource. SEND already knows it is a POST to that path. You do not keep the method in your head when you write a template or call app.add. Both take the same object. The path and the verb are written once, so there is no second place to get them wrong.
UrlPath is still the thing you compose, prefix for middleware, and pass when you only need a location. Route is the leaf you register and call.
That is the shape I want in our apps from here. New endpoints are Route.get / Route.post / … and app.add. app.get(path, handler) still works; it is no longer the default I will write.
4.1 also adds HTTP QUERY (Route.query, app.query) for a safe request with a body. Datastar has no @query action, so at.fetch does not emit one. Details live in the routing reference and the changelog.
Ox — learning Datastar by building a smaller one
Ox is a teaching aid. I built it while I was stuck in Datastar internals. I wanted to understand signals — how a write schedules work, how a read tracks, how a batch holds the DOM still — then morphing, then the request that comes back as SSE. The only way that stuck was to implement them.
The joke is the API. Elements talk with moo=. The library is still ox. After that, it is ordinary JavaScript on the element: one st bag, raw objects, less convenience. No second grammar. The SSE wire matches Datastar on purpose, for now, so a Stario handler can speak to either client.
Right now that is the job: a thinner cut of the same ideas, so the guide and cookbook have something small to point at. If you like it, we can grow the concepts and make it more production-ready. If you already have Datastar, you can still learn something here — the pieces are just less wrapped. Copy ox.min.js into the app if you want to poke at it.
Orbit — a sequence, not a model
Orbit is a deploy step runner. It is the result of trying to make deployment feel the same across apps that do not look the same.
I kept meeting the same wall: a system that tries to describe every application by configuration grows a large exception list. Most of the time I needed a script, in order, on a machine I can SSH to. If a step fails, I can fix it in the script or run it by hand. That is more honest than a planner that almost fits.
So Orbit is Git, rsync, and bash. It uploads the work tree into a named slot, exports a few stable paths and env vars, and runs ops/deploy.sh. There is no remote Orbit daemon. The application already knows how to build, start, check, and stop itself. Orbit only standardizes the outer steps.
The second idea is the slot. One repository, several places it can run: production, staging, a short-lived preview, a different configuration of the same app. You do not fork the application to get a second instance. You create another slot and give it its own env. The hooks stay the same.
That is the whole product: guide, hooks, a Podman recipe, and the source.
Where to go
Stario 4.1 —
CHANGELOG.md, routing, the manual
If you only take one thing from 4.1: declare the endpoint as a Route, then use that value on the server and in the page.
— Adam