13 KiB
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
completedSlotsfor 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:SSformat. - 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
pendingByon 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
localStorageunder 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, andjoinedAtare 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:
- Scan the code
- Get your number
- 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
<img>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.