Saturday Wash Queue
A mobile-first web app that manages OOOG's Saturday car wash queue at the Cole Street garage. Members book time slots from their phone. Staff run the queue. Everything is automated — SMS updates, Slack notifications, and a permanent wash history database.
What It Is
The problem it solves: replacing a Saturday wash process that was a bit chaotic, not fair to the staff or the members. Members physically arrive at the garage, scan a QR code, pick a time slot, and receive SMS updates when their car is up next and when it's ready. Staff manage the queue from their phones without any manual data entry.
Scan QR → pick a slot → enter name, car, phone → receive confirmation text with a link to manage the booking. Get texted when up next and when done.
Open queue → watch slots fill in real-time → tap each slot to advance it through Queued → Up Next → Washing → Done. SMS and Slack fire automatically.
The Five Views
| URL | View | Who uses it | Access |
|---|---|---|---|
| / (default) | Member queue | Members at garage | GPS within 500m or QR code |
| /?qr=ooog-saturday-wash | Member queue (QR) | Members who scanned QR | GPS within 200m (tighter) |
| /?view=staff | Staff dashboard | Staff running the wash | Staff PIN |
| /?view=embed | Read-only widget | OOOG Webflow members page | None |
| /?manage=TOKEN | Booking management | Member who booked | Unique token in SMS |
Architecture
Everything runs on Cloudflare infrastructure. There is no traditional backend server. The Worker is a serverless function that wakes up only when a request hits it.
Monday.com is not used for real-time data. The live queue lives entirely in Cloudflare KV. Monday only receives one write at end-of-day reset — it's a reporting dashboard, not a data store.
Why This Stack
| Component | Why chosen | Alternative that was rejected |
|---|---|---|
| Cloudflare KV | Sub-millisecond edge reads, no rate limits for 15 slots, single JSON blob = single read/write | Monday.com — rate limited at 30 calls, caused 429 errors |
| Cloudflare D1 | SQL queries on wash history, same infrastructure, free tier | JSONBin — no SQL, no querying |
| Single HTML file | No build step, no npm, instant deployment, zero dependencies | React/Next.js — unnecessary complexity |
How It Works — The Core Concept
The entire live queue is one JSON blob stored in Cloudflare KV under the key WASH_QUEUE_STATE. It contains all 15 slots and their current data. Every action in the app reads this blob, updates the relevant slot, and writes it back.
{
"sessionDate": "2026-06-07",
"slots": [
{
"id": 1, "slotTime": "10:00 AM", "statusIndex": 4,
"memberName": "Sarah M.", "vehicle": "2022 Porsche 911 GT3",
"memberPhone": "5865550101", "droppedOffAt": "2026-06-07T14:05:00Z",
"washStartedAt": "2026-06-07T15:05:00Z",
"washDoneAt": "2026-06-07T15:38:00Z",
"bookingToken": "k4xm92bq", "notifiedUpNext": true, "notifiedDone": true
},
... 14 more slots
]
}
Slot Status Values
| Index | Label | Displayed as | Slot number colour |
|---|---|---|---|
| 1 | Up Next | Up Next | Amber |
| 2 | Queued | Queued | Light grey |
| 3 | In Progress | Washing | Green |
| 4 | Done | Done | Faded (28% opacity) |
| 5 | Available | Available | Black |
| 6 | Cancelled | Cancelled | — |
| 7 | Unavailable | Unavailable | — |
Member Booking Flow
- Scan QR code at the garage — or navigate directly to the URL. GPS check runs immediately on page load (prompts browser permission).
- View the queue — available slots show in black with "Tap to book". Claimed slots show "Claimed". Time-expired slots show "Unavailable".
- Tap an available slot — geo validation runs (200m if QR, 500m if direct URL). If not within range, an error message appears. No form opens.
- Fill in the form — name, vehicle, mobile number, optional notes for staff.
- Submit — worker validates slot is still available, generates a booking token, saves to KV. If slot was taken in the meantime, an error message appears.
- Receive confirmation SMS — "Your OOOG wash is booked — Slot X · est. TIME. Need to swap, cancel or message staff? [manage link]"
Slots grey out 30 minutes after their estimated start time — on Saturdays only. A member cannot book an expired slot. Staff can still manually add to any slot regardless of time.
Staff Queue Management
Opening the Day
- Go to
/?view=staff— enter the staff PIN. - Tap Est. Wait / Car chip (top left) — set the expected wash duration (20, 30, 45, or 60 min). This drives the estimated wait time members see.
- Tap Open Queue — members can now see and book slots. Nothing is automated — the queue only opens when staff opens it.
Working the Queue
Tap any slot row to open the detail sheet. Available actions depend on the slot's current status:
| Status | Available actions | Automatic triggers |
|---|---|---|
| Queued | Mark Up Next, Start Wash, Edit Details, Swap Key Tag, Cancel | — |
| Up Next | Start Wash, Edit Details, Swap, Cancel | SMS sent to member on Mark Up Next |
| In Progress (Washing) | Mark Done, Edit Details, Cancel | Wash start timestamp recorded |
| Done | Read-only | SMS sent to member, wash done timestamp + duration recorded |
| Available | Add Member Manually | — |
Admin Tab
Accessible as a tab within the staff view. Provides: summary stats, force status override for any slot, manual SMS resend, and the weekly reset.
The Reset Queue button is blocked if any slot is marked In Progress (Washing). Mark the car Done or Cancelled first.
Member Booking Management
Every booking generates a unique 8-character token stored in KV. The token is sent to the member in their confirmation SMS as a link: https://ooog-wash-queue.pages.dev/?manage=TOKEN
From the manage page, members can:
- Swap to any available non-expired slot — auto-executes if the current wash hasn't started
- Request cancellation with a reason — staff must confirm before the slot is freed
- Message staff — one-way, posts to Slack and is appended to the slot notes in KV
The manage link expires (shows "Your wash is complete") when staff marks the slot Done or Cancelled. Time of day is not a factor — expiry is purely status-based.
Saturday Morning Runbook
| Time | Action | Notes |
|---|---|---|
| Before 10 AM | Open ?view=staff, set wait time, tap Open Queue | Nothing is automated — must be done manually |
| 10 AM–2:30 PM | Members scan QR and self-register as they arrive | Monitor slot list in real-time |
| Throughout | Advance each slot: Queued → Up Next → Washing → Done | SMS fires automatically at Up Next and Done |
| When done | Close Queue (stops new registrations) | Existing bookings are unaffected |
| End of day | Admin tab → Reset Queue | Archives to D1, sends to Monday, creates 15 fresh slots |
Data Layer
Live State — Cloudflare KV
KV holds the current Saturday's queue as a single JSON blob. Reads are edge-cached and near-instant. There are no rate limits relevant to this use case.
| KV Key | Value | Purpose |
|---|---|---|
| WASH_QUEUE_STATE | JSON blob of 15 slots | The entire live queue |
| WASH_QUEUE_OPEN | "true" or "false" | Whether members can book |
| WASH_WAIT_MINUTES | Number as string | Staff-set estimate per car |
Permanent Archive — Cloudflare D1
D1 is a SQLite database. One record is written per active slot at end-of-day reset. Never modified after writing.
SELECT slot_num, member_name, vehicle, status,
dropped_off_at, wash_started_at, wash_done_at, wash_duration_mins
FROM wash_sessions
WHERE session_date = '2026-06-07'
ORDER BY slot_num;
The wash_duration_mins column is the number of minutes between wash start and wash done — calculated automatically at reset time.
Data Journey
Notifications
SMS (SimpleTexting)
The SimpleTexting API endpoint is https://app2.simpletexting.com — note app2, not app. The app URL redirects and strips the Authorization header, causing silent 401 failures.
| When sent | Message | Dedup logic |
|---|---|---|
| Member books (self or staff add) | "Your OOOG wash is booked — Slot X · est. TIME. Need to swap, cancel or message staff? [link]" | None — always sends |
| Staff marks Up Next | "You're up next at OOOG! Slot X. Head over when ready. 🚗" | notifiedUpNext flag prevents resend |
| Staff marks Done | "Your car is ready for pickup at OOOG! Keys at the front desk. — The Team 🚗" | force=true bypasses dedup |
| Slot swapped | "Your OOOG wash slot has been updated — Slot X · est. TIME. Manage: [link]" | None |
Slack (#saturday-wash)
Every meaningful action posts to Slack: booking, Up Next, Washing, Done, cancellation, swap, force override, queue open/close, reset. Staff can monitor the full day without opening the app.
Geo-Gate & QR Code
The geo check prevents members from booking from home. It runs twice — once client-side (fails fast before form opens) and once server-side (worker validates on every QUEUED registration).
| Access method | Radius checked | Where |
|---|---|---|
QR code scan (?qr=ooog-saturday-wash) | 200m | Client + Server |
| Direct URL (no QR) | 500m | Client + Server |
| Staff manual add | Bypassed entirely | staffIntake: true in payload |
The QR code URL posted in the Cole Street garage:
https://ooog-wash-queue.pages.dev/?qr=ooog-saturday-wash
The token ooog-saturday-wash is hardcoded in both the HTML and the worker. To change it, update both const QR_TOKEN values and regenerate the QR code.
Phones use GPS + WiFi + cell towers for location. Indoor GPS can be inaccurate (50–300m). If a member is physically at the garage but gets a "too far" error, they should step outside briefly or speak to staff for a manual add. Accuracy is checked — GPS readings with accuracy > 150m are rejected with a "weak signal" message.
Worker API Routes
| Route | Method | Purpose |
|---|---|---|
| /wash-slots | GET | Read full queue state from KV |
| /wash-slots/:id | POST | Update one slot (id = 1–15) |
| /wash-photo | POST | Anthropic Vision OCR for keychain number |
| /wash-notify | POST | Send SMS via SimpleTexting |
| /wash-slack | POST | Post to Slack webhook |
| /wash-admin | POST | open / close / reset / init / pin-check |
| /wash-settings | POST | Update WASH_WAIT_MINUTES in KV |
| /wash-manage | GET | Look up booking by token |
| /wash-manage | POST | Swap / cancel request / message |
| /potluck | GET / PUT | Potluck signup list (unrelated — do not touch) |
Deployment
HTML App (Cloudflare Pages)
- Edit saturday-wash.html (the source of truth)
- Run:
cp saturday-wash.html deploy/index.html - Upload the
deploy/folder to Cloudflare Dashboard → Workers & Pages → ooog-wash-queue → Upload assets
Worker (Cloudflare Worker)
- Edit ooog-member-tools-worker.js
- Open Cloudflare Dashboard → Workers & Pages → ooog-member-tools → Edit Code
- Select all → paste full file contents → Save and Deploy
- Ensure format is set to ES Module (not Service Worker)
Run this once to initialise the KV queue state:POST /wash-admin {"action":"init"}
Secrets Required in Worker
| Secret name | Where to get it |
|---|---|
| SIMPLETEXTING_API_KEY | SimpleTexting → Account → API |
| SIMPLETEXTING_SENDER | SimpleTexting outbound number, digits only |
| SLACK_WASH_WEBHOOK_URL | Slack → Apps → Incoming Webhooks |
| ANTHROPIC_API_KEY | Anthropic Console |
| MONDAY_API_KEY | Monday.com → Admin → API |
| WASH_STAFF_PIN | Set by admin — any numeric PIN |
| JSONBIN_API_KEY / BIN_ID | JSONBin (for potluck — do not change) |
Common Issues & Fixes
Member says they didn't get a text
- Check the phone number was entered correctly during booking.
- Check SimpleTexting account → Messages for any send errors.
- Use the Admin tab → Manual SMS Resend to force-send the Up Next or Done text.
- If booking confirmation is missing, the slot can still be worked normally — texts only require a valid phone number in the slot.
Member can't book — "too far from garage" error
- Ask them to step outside the building — indoor GPS is often inaccurate.
- Ask them to check Location is enabled for their browser (Chrome: tap padlock → Site Settings → Location → Allow).
- If still blocked, use Staff manual add on their behalf.
Staff can't manually add a member — nothing happens
This was a known bug (fixed). Ensure the latest worker and HTML are deployed. The issue was a reference to a removed HTML element (ma-keytag) that silently crashed the submit function.
Queue shows "ask staff" for estimated wait time
Staff haven't set the wait time, or the embed view is showing stale data. Staff should tap the Est. Wait / Car chip and set a value. This was also a fixed bug where the embed view didn't read the wait time correctly — ensure latest HTML is deployed.
Worker returns 429 (rate limited)
This cannot happen with the current architecture — 429s were from the old Monday.com integration. KV has no relevant rate limits for this use case. If a 429 appears, check which API is being called.
Reset fails — "Cannot reset — wash in progress"
One or more slots has status In Progress (Washing). Open the staff view, find the slot, and mark it Done or Cancelled before running reset.
D1 archive shows no data after reset
Check the D1 console — if wash_sessions table exists but has no rows, run the reset again with an active booking. If table doesn't exist, run the CREATE TABLE SQL in D1 console (see CLAUDE.md).
What to Watch
Verify SimpleTexting account has credits. Confirm Slack webhook is active. Check Cloudflare Worker is deployed and responding (GET /wash-slots should return slots).
Monitor the #saturday-wash Slack channel — every action posts there. If Slack goes quiet when you expect activity, something may be wrong.
Confirm D1 has new rows for the day. Confirm KV shows 15 fresh Available slots (GET /wash-slots). Confirm Monday.com received EOD summary items.
Check Cloudflare Worker request count (free plan: 100K/day). Check SimpleTexting message credits. Rotate WASH_STAFF_PIN if staff changes.
Rotating Secrets & Keys
All secrets are stored as encrypted environment variables in the Cloudflare Worker — never in the HTML or code. To rotate a key:
- Get the new key from the relevant service (SimpleTexting, Slack, Anthropic, Monday).
- Go to Cloudflare Dashboard → Workers & Pages → ooog-member-tools → Settings → Variables.
- Edit the encrypted variable, paste the new value, save.
- No redeployment needed — the worker picks up the new value immediately on the next request.
Change const QR_TOKEN = 'ooog-saturday-wash' in both saturday-wash.html and ooog-member-tools-worker.js. Redeploy both. Generate a new QR code with the new token. Replace the physical QR in the garage.
The staff PIN is set as WASH_STAFF_PIN in the worker secrets. Changing it in the dashboard takes effect immediately — no redeploy needed. The PIN gates direct access to /?view=staff. The Webflow page Memberstack gate is the primary access control.