# Guerrilla Queue — Technical Specification ## Concept A zero-administration, self-organizing digital queue. Deployed as a single URL, accessed via a printed QR code. No login, no backend operator, no persistent management UI. Strangers coordinate themselves through a shared live view and social contract enforced by the interface copy. The digital queue is a coordination aid, not an obligation. People without phones participate through physical presence and social interaction — the queue reflects reality, it does not enforce it. --- ## Architecture Overview - **Single-page application** — one URL handles everything. No routing needed. - **Realtime shared state** — all connected clients see the same queue, updated live without page refresh. Use WebSockets or a realtime database (e.g. Firebase Firestore, Supabase, or a simple WebSocket server with in-memory state). - **No authentication** — identity is a randomly generated token stored in `localStorage`. This token is the only proof of ownership of a queue entry. - **No admin interface, no privileged roles.** All users are equal. - **Ephemeral by design** — the queue does not need to survive server restarts. Persistence across sessions for individual users (via localStorage token) is sufficient. --- ## Data Model ### Queue Entry ``` { id: string, // server-generated unique ID for this entry token: string, // secret random token, stored only in client localStorage // may be absent if entry was created by someone else's re-scan number: integer, // sequential ticket number, assigned at creation, never reused name: string?, // optional display name, set once, not editable joinedAt: timestamp, status: enum, // "active" | "pending_removal" pendingBy: integer[]?, // list of queue numbers who voted/triggered removal pendingAt: timestamp? // when pending_removal status was set } ``` ### Queue State (global) ``` { entries: Entry[], // ordered by number ascending counter: integer, // monotonically increasing ticket counter, never resets completedSlots: Duration[] // list of durations of completed (self-removed) entries, // used for wait time estimation } ``` --- ## Core Flows ### 1. Opening the URL - Default view: **the queue is shown immediately**, full screen. - If the user already has a token in localStorage that matches an active entry → their entry is visually highlighted. Their "I'm done" and removal controls are visible. - If the user has no active entry → a prominent **"Get my number"** button is shown. It does not block reading the queue. ### 2. Joining the Queue - User taps "Get my number". - Optional: a name input appears inline (single text field, placeholder "Your name or nickname — optional"). A "Skip" / "Join" split action. - Server assigns the next sequential number and returns the entry + a secret token. - Token is saved to localStorage. Entry appears in the live queue immediately. - The user's entry is visually distinct (highlighted / "this is you" label). - **Adding someone else to the queue:** re-scan the QR code on a second device (or hand the phone with the URL to another person). Each scan produces a new independent entry with its own token. Sequential numbers and close timestamps make the relationship self-evident to others in the queue. No special "add after me" UI is needed. ### 3. Self-Removal ("I'm done") - Shown only on the user's own entry (matched via localStorage token). - Single tap → inline confirmation ("Done? This removes you from the queue.") → entry deleted for all clients immediately. - Server records the duration of this slot (joinedAt → now) into `completedSlots` for wait time estimation. - No undo. ### 4. Wait Time Estimation - Each time a user self-removes via "I'm done", the server appends the duration of their slot to `completedSlots`. - Average slot duration is computed from all recorded completions. - Each entry in the queue displays an estimated wait: `position × average_duration`. - Position is zero-indexed from the front (position 0 = currently #1, estimated wait = 0 or "you're next"). - If fewer than 3 completions have been recorded, show "?" — not enough data yet. - Displayed as a rough hint, not a promise. Label it explicitly: "~8 min (estimated)". - Only self-removals count toward the average. Voted-out entries are excluded — they don't represent normal service time. ### 5. Timestamps - Every entry displays its absolute join time: `HH:MM:SS` format. - No relative timestamps ("3 min ago"). Absolute time is unambiguous and directly useful for coordination — people can ask "who has 11:23?" without any app logic. --- ## Removal Mechanics Two cases, both using the same pending/restore window. All entries are treated identically regardless of whether they have an owner token — the app makes no visible distinction. ### Case A — Removing the #1 entry (anyone can act, single tap) The most common legitimate case: the person at the front is visibly gone. - Any user in the queue can tap "Mark as done" on the #1 entry. - The entry immediately enters **pending_removal** state: - Visually dimmed, labeled "Marked done by #NN — removing in 60s" - A public countdown is shown - If the entry owner has the page open, they see a prominent banner: *"#NN marked you as done — was this a mistake?"* with a **Restore** button. - Tapping Restore returns the entry to active status instantly, visible to all. The pending_removal state and countdown are cleared. - After 60 seconds with no restore → entry is permanently deleted. - If the entry has no owner token (was created by someone else's re-scan), the restore banner simply never appears — the 60s window passes and the entry is removed. No special handling needed. - Attribution (#NN who triggered it) is always shown — named accountability discourages abuse. ### Case B — Removing a middle-queue entry (quorum vote) For entries not at position #1. Higher bar required because absence is less obvious and removal is more disruptive. - Any user in the queue can tap "Vote to remove" on any non-#1 entry. - Their queue number is added to `pendingBy` on that entry. - **A user may only have one active removal vote at a time across the entire queue.** If they try to vote on a second entry while their first vote is still pending, the action is blocked with a message: *"You're already voting to remove #NN."* This prevents bulk voting and coordinated abuse. - Removal threshold: **3 votes OR 50% of total active queue size, whichever is smaller.** - 4-person queue → 2 votes needed - 20-person queue → 3 votes needed - While votes are accumulating, the entry is visually marked "N/M votes to remove". - Once threshold is reached → entry enters **pending_removal** state, same 60s restore window as Case A. - If the entry owner restores: votes are cleared, entry returns to active. The voters' vote slots are freed — they may vote on another entry. - If removed: the owner's restore banner shows the full list of who voted: *"#4, #6, #8 voted to remove you."* - If the entry has no owner token, the restore banner never appears — 60s passes and the entry is removed. - Users may not vote on their own entry. - No vote retraction in MVP. --- ## Identity & Session - On first join, the server generates a `token` (cryptographically random, e.g. 128-bit hex). - Token is returned to the client and stored in `localStorage` under a fixed key (e.g. `queue_token`). - On every page load, the client sends the token to the server to recover its entry reference. - If the token matches no active entry → client silently drops the stale token and presents the "Get my number" flow. - **The token is never shown to the user and never sent to other clients.** Only `id`, `number`, `name`, and `joinedAt` are public. - Accidentally closing and reopening the browser recovers the session naturally via localStorage. --- ## Anti-Spam - One active entry per token. If a client with an existing active token tries to join again, the server rejects and returns their current entry instead. - This does not prevent helping others: each device that scans the QR gets its own independent token and entry. A person helping someone without a phone simply scans on their behalf on a second device or hands their phone to them. - No IP-based limiting — unreliable and overly aggressive for this context. --- ## The Printed QR Sheet This is part of the product. The physical paper should include: - The QR code (large, center). - A short headline: e.g. *"Tired of the chaos? — Take a number."* - 3-line instruction: 1. Scan the code 2. Get your number 3. Remove yourself when you're done - The URL in plain text below the QR code (for people who won't scan). The application UI should be consistent with this framing — it's a social proposal, not a corporate system. --- ## UI / UX Constraints - **Mobile-first.** Large tap targets, minimal scrolling. - **Queue visible before any interaction.** Never show a splash or join screen before the queue. People need to see it to trust it. - **Copy matters.** "I'm done" not "Leave queue". "Get my number" not "Join". Interface should feel like a helpful stranger, not enterprise software. - **No modals.** All interactions (join, confirm removal, vote) happen inline within the card or as expansion of existing UI. - **No loading spinners on main queue view.** Realtime updates should feel instantaneous. Skeleton state on initial load is acceptable. - **Timestamps are absolute:** `HH:MM:SS`. No relative time display. - **Pending removal state** must be visually unambiguous — dimmed entry, countdown timer, attribution clearly labeled. - **Vote count on middle-queue entries** is always visible once at least one vote exists: "2/3 votes to remove". - **Blocked vote attempt** (user already has an active vote elsewhere) should fail gracefully inline, not silently. --- ## Edge Cases to Handle | Scenario | Behavior | |---|---| | User clears localStorage | They lose their entry. They can rejoin but their old entry persists until quorum removes it or it reaches #1 and gets single-tapped. | | User closes tab and reopens | Token recovered from localStorage, entry highlighted as before. | | Two tabs same browser | Both recover the same token, both highlight the same entry. Removal from either works. | | Queue is empty | Show empty state with clear "Be the first" prompt. | | Single entry in queue | Normal display. No vote controls visible (no other users to vote). | | Entry has no owner token (created by re-scan) | Treated identically to owned entries in all removal flows. Restore window passes silently with no notification. | | Entry in pending_removal changes position | Pending state is positional-agnostic. Countdown continues regardless of position change. | | All entries are pending_removal simultaneously | Unlikely but valid. Each has its own independent countdown. | | User tries to vote on two entries | Second vote blocked with attribution to their active vote. | | Voter's entry is removed while their vote is pending | Their vote is voided. The threshold recalculates without them. | | Server restart | Queue lost. All localStorage tokens become stale, silently dropped on next load. | | Very long queue (50+ entries) | Single scrollable list. No pagination in MVP. | | Not enough data for estimation | Show "?" for all wait times until 3+ completions recorded. | --- ## Frontend Size Constraint (optional, highly desirable) The frontend should aim to fit within **16 KB uncompressed** so it loads fast even on poor mobile signal — a realistic condition for people standing in a corridor scanning a QR code. This is achievable with the following constraints: - **Vanilla JS only.** No frontend framework (React, Vue, etc. cost 40–130 KB before any app code). - **Native WebSocket.** Built into every browser. Do not use Socket.io or any WebSocket wrapper library. - **No npm dependencies in the frontend bundle.** If the browser provides it natively, don't bundle it. - **System fonts only.** `font-family: -apple-system, sans-serif`. No Google Fonts or external font imports. - **No icon libraries.** Unicode symbols (✓ ✕ ⏳ etc.) cover all UI needs. - **QR code generated server-side**, served as a plain `` tag. No QR generation library in the frontend. - **Gzip or Brotli compression enabled on the server.** With gzip, a 14 KB uncompressed bundle becomes ~4–6 KB over the wire. If a deliberate tradeoff requires exceeding 16 KB uncompressed, document the reason explicitly. --- ## Out of Scope (explicitly) - Admin panel or operator controls - Notifications or push alerts - Queue persistence across server restarts - Multiple simultaneous queues - Authentication or accounts - Entry editing after creation - Vote retraction - Any distinction in UI between owned and unowned entries --- ## Success Criteria A stranger with no context scans the QR code, understands what the app does within 5 seconds, joins the queue in under 15 seconds, and knows how to remove themselves when done — without reading any documentation.