Skip to content

Showcase

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.

Try the live demo Read the code Every page and its features

Take a look

Try both sides

Register a customer account, or log in as [email protected], or as [email protected] or [email protected] for the staff side; every demo account's password is “password”, and the login page lists the others. The demo is reset every hour, and it sends no mail.

Measured

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

How it stays fast

Pages come from the server

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"))

One query per kind of data

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 dashboard is cached

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.

Slow work waits in the queue

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.

Assets travel once

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.

The database is next door

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.

Fast, and still correct

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.

How we measured

See what else Renox does

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.

Explore renox.rs The cheat-sheet