Ok(view("catalog/index.html", context! { cards, filters, … })
.fragment("results"))
The shop's front doorBuy, rent or book a service; the panel explains the page
A whole bike business in one binary: a shop, rentals, a workshop, service plans and the back office of three stores, built with Renox.
The bike shop is Renox's flagship example. Customers buy bikes and gear, rent a bike by the hour or the day, and book services or a plan that books them for them. Staff in three stores run the counter, the fleet, the workshop board, the stock and the reports, each with the permissions their role gives them in that store. It is a complete app, tested end to end, and it runs from one executable with SQLite next to it.
Every page explains itself: on a wide screen a panel beside it says what the page is for, which Renox features it uses and why, and shows the code that makes it work.
The shop's front doorBuy, rent or book a service; the panel explains the page
The catalogueFilters and sorting; htmx swaps only the results
A productSizes and colours, with stock in each store
Renting a bikeThe bikes free for the whole period, with their price
The workshop boardWork orders moved by dragging or the keyboard
The reportsFigures and charts drawn on the server, no chart library
Try both sides
A release build of Renox 1.1.1 with the demo's large seed: 1,016 product variants, 601 customers, 15,412 rentals, 5,000 orders and 3,002 work orders. How we measured is at the bottom.
Queries for a product list
13
for 4 products or 24; one per kind of data
A page, with its code
111 KB
14 KB gzipped; a filter answers with 12 KB
JavaScript and CSS
78 KB
gzipped, fingerprinted, cached for a year
The whole app
62 MB
one binary: code, views, CSS, JS, migrations
Every page is rendered in Rust and arrives complete: there is no front-end build and no JSON API behind the pages. Changing a filter asks with htmx, and the same handler answers with only the results, a tenth of the page:
Ok(view("catalog/index.html", context! { cards, filters, … })
.fragment("results"))Relations are explicit, so nothing runs a query per row. A list of product cards loads brands, categories, variants and photos in four queries, however many products it shows:
let photos = has_many(
db, &products, ProductPhoto::query(),
"product_id", |p| p.product_id).await?;The reports are computed in a fixed number of queries and kept with Cache::remember. Listeners mark them stale when a rental closes, a work order is collected or a payment comes in, so the figures are never out of date and a second visit runs almost nothing.
Mail to customers is queued, so a slow mail server never slows a checkout, and the monthly report is a batch of jobs whose progress the page polls. The queue and the scheduler run inside the same process: no Redis, no separate worker to deploy.
htmx, Alpine.js and the UI kit are served from the binary with a fingerprint in their names and a one-year immutable cache. After the first visit, a page is just its HTML.
SQLite runs inside the app's process, in WAL mode, so a query is a function call rather than a network round trip. The same code runs on PostgreSQL when one machine isn't enough.
Speed is no use if two customers can rent the same bike for the same afternoon. The form checks first, so the customer sees "just taken" next to the field. Then the transaction that writes the rental takes the bike's row and checks again, because another booking may have landed in between: two bookings of one bike run one after the other, and the second sees the first.
let mut tx = db.begin_immediate().await?;
// SQLite's write lock, PostgreSQL's row lock:
let Some(bike) = RentalBike::query()
.where_(RentalBike::ID.eq(new.bike_id))
.lock_for_update()
.first(&mut tx)
.await?
else {
return Ok(Err(Refusal::Gone));
};
if clashing(bike.id, new.start, new.end).exists(&mut tx).await? {
return Ok(Err(Refusal::Taken)); // rolled back, nothing written
}
// ...the rental, with its price and deposit, then
tx.commit().await?;The rental keeps the store that owns the bike and the store that rented it out, so the books between the stores never change when a bike moves later.
examples/bikeshop at Renox 1.1.1, built with cargo build --release (80 MB, 62 MB stripped), then demo:seed --size large, as on the demo. Serving these pages it used about 60 MB of memory./_renox/debug, on the second visit: a product list runs 13 (14 in a category, for 4 products or 24), a search 13, a product page 7, the home page 8.curl, and gzip -9 for the compressed figures: the app sends plain responses and leaves compression to the proxy in front of it. Each page carries its "About this page" panel with code samples, as on the demo.Guides for every part (routing, forms, models, queues, mail, data grids, testing, deploying) and a tutorial that builds an app from rnx new to a server.