This page traces every flow that moves on its own or on a single click. Read it before you read a number in Metrics: a metric only means something once you know which step wrote it.
How automation runs here
Space has no scheduler: no cron trigger in wrangler.jsonc, no queue worker, no email. Work that happens "by itself" happens in one of three ways.
| Mechanism | When it fires | Examples |
|---|---|---|
| Lazy on read | Every time a page or query evaluates a condition | Booking and ticket holds expire, program and opportunity deadlines close, packages expire |
| Daily routines | On the first /dashboard/desk page load of each Port Sudan day | Rent invoices, forgotten check-outs, bookings completed or cancelled |
| In the transaction | Inside the action that the guest or staff member triggers | Waitlisting at capacity, room clash lock, free tickets issued instantly |
Booking and café events also send notifications by themselves — the in-app bell, push to the phone, WhatsApp and email, each when configured; see Notifications. The other outbound links are WhatsApp deep links (wa.me): the guest or the staff member taps one and sends the message themselves, and no tap is recorded.
If nobody opens the desk on a given day, that day's routines do not run. They run the next time the desk is opened, and they only ever record the latest day. Check DeskSettings.routinesRanOn — see the automation health query in Metrics.
Legend
States in green are reached by the system. Grey states are set by a person. The second row lists the states a record can leave the path to.
Booking
A guest picks a product and time on /book. createOnlineBooking quotes a price when there's a set rate, takes a Postgres lock on the room, checks for clashes, and sends the guest to their booking page.
| Step | Who | Recorded |
|---|---|---|
Request created; a priced room is held for ticketHoldMinutes (default 120) | Guest | createdAt, amount, holdUntil |
| Guest submits their Bankak transaction number | Guest | bankakReference, paymentSubmittedAt |
| Staff verify the transfer → charge and payment written to the ledger | Desk (ADMIN, CASHIER) | DeskCharge.createdAt, DeskPayment.createdAt |
| Staff reject the transfer | Desk | cancelReason (no time) |
| Day-pass guest checks in → CONFIRMED | Desk | DeskVisit.checkedInAt |
Auto: confirmed and past endsAt → COMPLETED | Daily routine | updatedAt only |
Auto: pending, never paid, past endsAt → CANCELLED | Daily routine | updatedAt only |
Lazy: hold passes holdUntil → slot freed, guest page shows "expired" | On read | no status change |
Dedicated desks, private offices and anything without a rate get amount = null: staff quote them by WhatsApp.
Tickets
Paid events sell tickets online or at the door. createOnlineOrder locks the event row, counts seats that are paid, submitted or still held, and caps the order at 10.
| Step | Who | Recorded |
|---|---|---|
| Order created and seats held | Guest | createdAt, holdUntil |
| Free ticket (price 0) → PAID and tickets issued at once | System | paidAt |
| Guest submits Bankak reference | Guest | bankakReference (no time of its own) |
| Staff verify → PAID, tickets issued, ledger written | Desk | paidAt |
| Door sale → PAID immediately (cash or Bankak) | Desk | paidAt, channel = DOOR |
| QR scanned at the door → admitted, once | Desk | Ticket.checkedInAt |
| Lazy: hold expires → seats released | On read | order stays PENDING forever |
Free event registration
Events without ticket types take registrations on the event page. Inside one transaction, registerForEvent counts REGISTERED rows: when the event is full, the new row is WAITLISTED.
Everything after sign-up is manual in the admin. There is no waitlist promotion when a seat frees up, and the row keeps only createdAt.
Program applications
applyToProgram accepts an application while the program is OPEN and before its deadline. The deadline closes the form lazily — the stored status never changes.
Enquiries
The contact, partners, community and membership forms all write an Enquiry with a topic (membership also stores the plan in subject). The guest can continue on WhatsApp straight away.
Front desk
- Visits. One open visit per customer. A day pass writes a
DAY_PASScharge; a package visit is checked against the package's minutes and expiry. Check-out rounds the minutes up and adds them to the package. - Forgotten visits. The daily routine closes visits left open from an earlier day at that day's closing time and notes "auto check-out at closing".
- Packages. Sold at the desk. Active, exhausted and expired are worked out on read.
- Offices. One active contract per office. Monthly rent invoices are issued automatically by the daily routine (or by the admin button), one per contract per month, due on the contract's due day. Overdue rent gets a WhatsApp reminder link.
- Ledger. Every charge (
DAY_PASS,PACKAGE,OFFICE_RENT,ROOM_HIRE,TICKET,OTHER) can take partial cash or Bankak payments. Voids are kept with a reason.
Café
Guests order by scanning a table QR (channel = QR); staff ring up the counter (channel = COUNTER, which starts confirmed).
Every status and payment change stamps its own time (confirmedAt, preparingAt, readyAt, completedAt, cancelledAt, paidAt) and writes a CafeOrderEvent row with the staff member. It is the only full event log in the product. Staff boards poll every 5 seconds and chime every 30 seconds while an order waits.
Report an issue
The dialog in the footer and the admin runs a pipeline: filters → rate limit → duplicate search on GitHub and AI triage (when a key is set) → a score. Reports scoring 30 or more become GitHub issues. Its telemetry goes only to the Worker logs.
Guards on every public form
- A hidden honeypot field, then a per-IP rate limit (5 submissions per 10 minutes by default; the café allows 60).
- A caught bot gets a fake success, so it doesn't retry.
- The limiter lives in memory per Worker, and nothing is counted — see Optimizations.