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.