个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Live demo: sixth-hour-cafe-queue.vercel.app
A real-time cafe order management system built with Next.js and Firebase — designed for a two-screen workflow where customers place orders on one iPad and staff manage the live queue on another.
Originally built for Sixth Hour Cafe, an Anchor Christian Fellowship at MSU pop-up cafe. This project was developed end-to-end as a personal project to solve a real operational problem: tracking handwritten drink orders is error-prone and slows down service during busy rushes.
It's multi-tenant: every cafe is a Sixth Hour Cafe, and each account is a different location (and the people running it) that the brand owner lets onboard their own isolated queue. The intended setup is a two-screen kiosk — a customer-facing tablet running the order page, and a staff tablet running the live queue — though the customer page can also be shared as a link or QR.
Landing (/) — Product intro with links to sign up / log in.
Sign in (/login) — Operators create an account or sign in with email/password or Google. New operators complete a quick setup (/onboarding) naming their location and choosing a unique public link slug.
Customer screen (/order/[slug]) — A cafe's public order page, typically opened on a customer-facing tablet (or reached via a shared link/QR). Customers tap their name, select drinks and modifications, and submit. No login required. The order appears on that cafe's staff screen in real time.
Staff queue (/staff) — The signed-in owner or staff member sees that location's pending orders as cards, with animated transitions when orders arrive or are completed. Each card can be marked done or deleted (with a one-tap Undo). A "Customer screen" button copies the order-page URL to open on the customer-facing device.
Sessions — Each pop-up service is a session with its own venue and start time. Starting one resets the drink counter; ending one freezes that session's totals. The History tab lists past sessions as stat cards (drinks, orders, top drinks) that open to the orders inside. A session can also be scheduled ahead of time — a future start time is saved without taking over as the active session, so planning next week can't capture tonight's orders. Orders placed while no session is running are still accepted and get attached to the next session that starts.
Menu management (/staff/menu) — Owners and staff add/remove drinks, update descriptions, mark items out-of-stock, reorder items, and manage categories and modifiers — all scoped to their own cafe, with auto-save and real-time sync.
Settings (/staff/settings, owner-only) — Rename the location, set the timezone used to date sessions, change the customer link, toggle and configure an Instagram QR shown after ordering, and manage staff.
Staff invites — From Settings, an owner generates an invite code/link. Anyone who signs up and redeems it at /join becomes staff for that location, with access to the queue and menu (location settings, link, Instagram, and staff management stay owner-only).
locations/{locationId}/…, with readable ids (locations/msu-campus, drinks/matcha-latte, date-prefixed sessions and orders) so the Firebase console reads at a glance. Firestore Security Rules scope all access to a single location, while menus stay publicly readable for customers.locations/{locationId}/members/{uid} doc with role owner or staff (staff join by redeeming an invite code). Owners and staff share queue + menu access; settings and the staff roster are owner-only — all enforced in the rules.onSnapshot listeners push order and menu updates to every connected screen instantly, with no polling.status: 'cancelled') with an Undo toast; a daily Vercel Cron job purges cancelled orders older than a week./api/orders) that writes via the Firebase Admin SDK. Security Rules deny client-side order creates entirely, so the route is the only path in. This keeps the customer screen login-free and lets App Check stay off Firestore reads so real-time updates stay instant.jose to avoid the firebase-admin app-check ESM/serverless issue — and validates the order's shape and size.qrcode.react), toggleable per location.| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Language | TypeScript |
| Database | Firebase Firestore |
| Auth | Firebase Authentication |
| Server | Next.js API routes + Firebase Admin SDK (order writes, public reads, cleanup) |
| Abuse protection | Firebase App Check (reCAPTCHA v3), verified with jose |
| Scheduling | Vercel Cron (daily cleanup) |
| QR codes | qrcode.react |
| Styling | Tailwind CSS + Radix UI primitives |
| Animation | Framer Motion |
| Forms | React Hook Form + Zod |
| Deployment | Vercel + Firebase App Hosting |
src/
app/
/ — Landing page
/login — Sign in / sign up (email + Google)
/onboarding — First-time setup (location name + public slug)
/join — Redeem an invite code to join a location as staff
/order/[slug] — Public customer ordering screen (per cafe)
/api/orders — Server route: verifies App Check + writes orders (Admin SDK)
/api/cron/cleanup — Scheduled purge of old cancelled orders (Admin SDK)
/staff — Queue dashboard (auth-gated; owner or staff)
/staff/menu — Menu and category management
/staff/settings — Location, link, Instagram, and staff management (owner-only)
components/ — UI components for customer and staff flows
lib/
data.ts — Server-side public reads (slug → cafe, menu) via Admin SDK
firebase-admin.ts — Lazy Firebase Admin SDK init (server-only)
cafe-paths.ts — Cafe-scoped Firestore path helpers + slug/invite utilities
definitions.ts — Shared TypeScript types
firebase/ — Client init, App Check, auth + cafe-membership providers
scripts/
migrate.ts — One-time legacy → multi-tenant data migration (Admin SDK)
vercel.json — Vercel Cron schedule for the cleanup job
npm install
cp .env.example .env.local
# Fill in your Firebase project credentials
npm run dev # Starts at http://localhost:9002
npm run build # Production build
npm run typecheck # Type check without emitting
Each location owns an isolated subtree under locations/{locationId}. The id is readable and fixed when the location is created (e.g. msu-campus); the public slug is separate and can change. The location field is the branch label (e.g. "MSU Campus"); the brand ("Sixth Hour Cafe") is fixed in the UI.
Document ids are chosen to read well in the Firebase console (src/lib/readable-ids.ts). Drinks, categories and presets are named after themselves (matcha-latte, matcha-latte-2 on a clash), so menuIds reads as a list of drinks. Sessions and orders start with their local date/time, so they list chronologically. Ids never change after creation; renaming a drink keeps its id.
| Path | Fields |
|---|---|
locations/{locationId} e.g. msu-campus | location, slug, createdAt, activeSessionId?, liveMenu?, timezone?, instagramEnabled?, instagramUrl? |
…/members/{uid} | email, role (owner / staff), code?, joinedAt — who has access; owner-only roster |
…/orders/{id} e.g. 2026-10-24_14-32-05_min_a1b2 | customerName, items[], status (pending / completed / cancelled; archived is legacy), sessionId, createdAt |
…/sessions/{id} e.g. 2026-10-24_library_k3f9 | location, startsAt, endedAt?, status (scheduled / active / ended), notes?, menuIds, soldOutIds, presetId?, and the stats frozen on end: drinkCount, orderCount, itemCounts |
…/drinks/{id} e.g. matcha-latte | name, description, category, inStock, order, modifications[] |
…/categories/{id} | name |
…/presets/{id} | name, drinkIds[] |
…/recipes/{drinkId} | ingredients[], instructions (staff-only) |
…/private/invite | code (owner-only; current invite code) |
users/{uid} | locationId, role, email — pointer from an account to its location |
Access is enforced by firestore.rules and decided by the member doc alone: location info, menus and categories are publicly readable; client-side order creates are denied — orders are written only by the /api/orders server route (Admin SDK). Owners and staff may manage drinks, categories, presets, recipes, orders, and sessions; location settings, invite codes, and the roster are owner-only — except activeSessionId and liveMenu, the location fields staff may change, so they can run sessions.
Moving from the old cafes/{uid} layout — scripts/migrate-readable-ids.ts copies every cafe into locations/, assigns readable ids, rewrites every reference to them, and builds members and users. See the header of the script for the run order (dry run → copy → deploy → catch-up → cleanup).
Testing rules and the migration — npm run test:emulator runs tests/ against the Firestore and Auth emulators: the security rules, and a full migration of seeded old-layout data. Needs Java on PATH (e.g. brew install openjdk).
Migrating existing data — scripts/backfill-sessions.ts reconstructs sessions from order history that predates the feature, grouping each cafe's completed orders into one session per calendar date in its timezone (and collapsing legacy archived orders back into completed). It's idempotent and dry-runs by default:
GOOGLE_APPLICATION_CREDENTIALS=/path/to/serviceAccount.json npm run backfill-sessions
GOOGLE_APPLICATION_CREDENTIALS=/path/to/serviceAccount.json npm run backfill-sessions -- --commit
The app is deployed on Vercel with Firebase as the backend. To deploy your own instance:
firebase deploy --only firestore:rules,firestore:indexes
FIREBASE_SERVICE_ACCOUNT (the full JSON on one line). This powers the server-side reads and the order API route.NEXT_PUBLIC_RECAPTCHA_SITE_KEY. Leave Firestore enforcement off (monitoring) — the order route verifies App Check itself, and unenforced reads keep real-time listeners instant..env.example to your Vercel project settings (for all environments you deploy, including Preview). Set CRON_SECRET to any random string so the cleanup cron is authenticated.A Vercel Cron job (see vercel.json) calls /api/cron/cleanup daily to purge soft-deleted (cancelled) orders older than a week.
If you're upgrading from the original single-cafe version, run the one-time migration to move legacy top-level collections into your new account. See the instructions in scripts/migrate.ts.
This project started as a practical tool for a real cafe and grew into a full-stack exercise in building a production-quality real-time web app from scratch — including data modeling, UI/UX decisions for touch screens, deployment configuration, and iterative feature development based on actual use.
slugs/{slug} | locationId |
inviteCodes/{code} | locationId (staff join lookup) |