Skip to content
Back to projects
svc/02personal

Cadence

A scheduling API in Rust: hosts publish weekly availability rules, invitees book the slots those rules generate, and confirmation and reminder emails leave through a transactional outbox so they can never describe a booking that did not happen.

Role
Solo
Year
2026
Runtime
RustAxumTokiosqlxPostgreSQLArgon2

What it is

A booking API. A host registers, describes when they are available as a set of weekly rules - weekday, start, end, slot length - and an invitee asks the API which slots exist over the next N days and takes one. Confirmation goes out immediately; a reminder follows an hour before the slot.

The interesting part is that no slot is ever stored. Availability lives in the availability table as seven-ish rows of rule, and slots_for_day walks a rule from its start time to its end time in slot_minutes steps, emitting only the slots that fit entirely inside the window. Changing a host's Tuesday means updating one row, not regenerating a calendar, and there is no job that has to keep a slot table topped up into the future.

How it's built

Axum on Tokio, Postgres through sqlx. Queries go through the query! and query_as! macros, so the SQL is checked against the live schema while the binary compiles: a column that gets renamed in a migration breaks the build rather than a request in production.

Two decisions carry most of the weight.

Conflicts are the database's job. bookings has UNIQUE (host_id, slot_start). Two invitees racing for the same slot do not need a lock or a read-check-write in the handler; one of them loses the insert, and the handler inspects the error, recognises the unique violation, and answers 409 with "That slot is already booked." A check-then-insert would have been shorter to write and wrong under concurrency.

Email goes through a transactional outbox. create_booking opens a transaction, inserts the booking, queues both messages into an outbox table, and commits. If anything fails, all of it disappears together. An SMTP call placed inline in the handler would have had the opposite property: the send can succeed and the commit still fail, and the invitee gets a confirmation for a booking the database does not have.

A separate Tokio task polls the outbox on a ten-second tick, claims up to twenty due rows, sends them through lettre, and stamps sent_at. Because the reminder is queued with send_after = slot_start - 1 hour, delayed mail and immediate mail are the same mechanism with a different timestamp, and finding due work costs an index scan over outbox_due_idx (send_after) WHERE sent_at IS NULL rather than a scan of everything ever sent.

Auth is deliberately dull: Argon2 with a per-user salt, a random UUID session token in a sessions table, and that token in an HttpOnly, SameSite=Lax cookie. CurrentHost implements FromRequestParts, so a route that needs a logged-in host writes host: CurrentHost in its signature and gets a 401 for free if there isn't one. The type system enforces the guard instead of a convention about calling a check function first.

Where it stands

It runs locally against Postgres and a local SMTP catcher. It is a back end with no front end, there are no tests yet, and it is not deployed anywhere, so the GitHub repository is the whole of it. I built it to work through the parts of Rust that only show up in something with state: FromRequestParts, error mapping across layers, a long-lived background task sharing a pool with request handlers, and the borrow checker's opinions about all three.

Back to projects