The reservation lifecycle is now a per-reservation Temporal workflow with durable timers and signals. No scan-and-act crons. No inline email sends gating user-facing responses. A customer reserves → the workflow takes over → the store member acts → email fires after each transition → the workflow exits at terminal state.
reservation-<uuid>. Every state mutation flows through a single guarded TransitionReservationActivity — status-checked UPDATE plus an audit row, both in one transaction. Returns Applied=false when the guard rejects, so replays and stale signals are no-ops.
kubectl apply.
/:locale/listing/:publicId/reserve on listing detail. HTMX form swap. Get-or-create users by email from Kratos session.
v0.13.0
reservations table every 5 minutes and every hour. The Go side runs zero such crons — durable timers fire at the exact second they should.
confirmed_expires_at − 1h./login. Admin routes go through AdminGate, then each handler scopes by the actor's membership store_ids — cross-store attempts return 404, no leak.
| Method | Path | Behavior |
|---|---|---|
| POST | /:locale/listing/:publicId/reserve | Insert reservation + kick ReservationWorkflow. Maps 5 sentinel errors to i18n fragments. |
| GET | /:locale/me/reservations | Tabbed list. ?tab=active|history with status chips + countdowns. |
| GET | /:locale/me/reservations/:publicId | Detail + reservation_events timeline + cancel button. |
| POST | /:locale/me/reservations/:publicId/cancel | SignalWorkflow(cancel). Returns HTMX swap fragment. |
| Method | Path | Behavior |
|---|---|---|
| GET | /:locale/admin/reservations | 3 sections: Pending (oldest-first), Confirmed, History. |
| POST | .../:publicId/confirm | Guard pending. SignalWorkflow(confirm) → confirmed-to-customer email. |
| GET | .../:publicId/reject-form | Inline reject modal: 4 reasons + note. |
| POST | .../:publicId/reject | Guard pending. SignalWorkflow(reject, reason, note). |
| POST | .../:publicId/fulfill | Guard confirmed. SignalWorkflow(fulfill) → fulfilled email. |
| POST | .../:publicId/no-show | Guard confirmed. SignalWorkflow(cancel-store). |
| Method | Path | Behavior |
|---|---|---|
| POST | /api/v1/internal/reservations/recover | Daily 04:00 UTC. Guarded sweep — flips past-TTL rows to expired + emits emails via Temporal. No-op under healthy operation. |
Non-admin store members can't reach /admin/reservations today even though the spec says "any membership can act." Swap AdminGate for a MembershipGate on this route group — handlers themselves are already membership-scoped.
Next.js had /admin/reservations/:id/notes for inline store-note edits. Not in the plan; v1.1 candidate.
Workflow tests cover the state machine via testsuite.WorkflowTestSuite. A round-trip test against a real DB needs the testcontainers env, which has been broken on dev machines for unrelated reasons.
If a confirmed reservation's workflow dies between hour 0 and 47, the customer never gets the pickup-reminder. The sweep catches expiry but the reminder window is closed. Acceptable for v1.
Initial deploy is v1. When the state machine first changes shape, use workflow.GetVersion to keep in-flight reservations consistent.
Temporal handles this cheaply in theory. Confirm under real load before peak season.