Build and play treasure hunts that unfold as a chat conversation.
Live: kwikquest.io to build · play.kwikquest.io to play
KwikQuest is two apps over one backend:
- Creator — authors build a quest: an ordered chain of clues, each with an answer, an optional hint, and custom responses for right and wrong guesses. Publishing yields a short access code.
- Player — players enter the code and work through the quest in a messaging UI. Clues arrive as incoming messages, answers go out as replies, and the next clue unlocks on a correct guess.
Quests can be anchored to physical places (a museum, a neighbourhood) or run entirely online.
The conversation is derived, not stored. A session holds an append-only log
of events — answer, hint, system. The client folds that log into a
conversation and replays it, animating incoming messages with typing delays so
a resumed session feels live rather than dumped on screen. Reloading mid-quest
restores from sessionStorage and skips the animation instead of replaying it.
Because the log is authoritative, the server can correct client state at any
point and the UI converges.
Answer matching is deliberately forgiving. Players type on phones, often
one-handed and outdoors, so exact string comparison would be hostile. Submissions
run through a layered match in
answerHelpers.ts: Unicode
normalisation and punctuation stripping, then article/filler removal, then
exact → substring → token-set overlap → Levenshtein distance with a
length-relative threshold that loosens for short answers (one typo in a
four-letter word is proportionally huge). All of it runs server-side.
Answers never reach the browser. The getQuest callable projects a
player-safe view of a quest — questions and a hasHint boolean, never the
answer or hint text. Firestore rules deny direct client access outright: no
browser code initialises the Firestore SDK, and every read goes through a
callable backed by the Admin SDK. Cheating means guessing, not opening devtools.
Guests are first-class. A cookie-issued localId lets people create and
play quests with no account, and claim them later by signing in.
| Frontend | React 19, TypeScript, Vite 7, MUI 7 |
| Forms/validation | React Hook Form + Zod |
| Data fetching | TanStack Query |
| Backend | Firebase Cloud Functions v2 (europe-west3), Firestore |
| Auth | Firebase Auth — email/password, Google, anonymous guest |
| Monorepo | Turborepo + npm workspaces |
Two apps (apps/creator, apps/player) and a functions codebase
(apps/functions) sit over seven shared workspace libs in libs/ —
chat (the messaging UI), authorization, ui (theme), firebase,
types, firestore (rules), sharedAssets.
Prerequisites
- Node 22 (
apps/functionspins this — the deployed runtime matches) - Firebase CLI:
npm i -g firebase-tools - A JDK — the Firestore emulator needs one
Setup
git clone https://github.com/tommoon/kwikquest.git
cd kwikquest
npm install
cp .env.example .env # then fill in your Firebase web configThe values come from Firebase console → Project settings → Your apps → SDK setup. They are not secrets; they ship in the client bundle either way. Access is controlled by Firestore rules, not by hiding them.
Run
npm run devThat starts both apps and the emulator suite in parallel:
| Creator | http://localhost:5173 |
| Player | http://localhost:5174 |
| Emulator UI | http://localhost:4000 |
The client points at emulators automatically in dev builds. Override with
VITE_USE_FIREBASE_EMULATORS=true|false.
To run one app on its own: npm run dev:creator or npm run dev:player.
To keep emulator data between runs, use npm run dev:persist -w functions — it
imports from and exports to apps/functions/emulator-data/, which is gitignored
(emulator auth exports store passwords in plaintext).
Other commands
npm run build # build all workspaces
npm run lint # lint all workspacesapps/
creator/ quest authoring app
player/ quest playing app
functions/ Cloud Functions — creatorApi/, playerApi/, helpers/
libs/
chat/ messaging UI component
authorization/ auth flows, account management, legal pages
ui/ MUI theme + toast provider
firebase/ client SDK init
types/ shared domain types
firestore/ security rules + indexes
sharedAssets/ images
A personal project, built to work end to end rather than to be exhaustive.
Known gaps: there is no automated test suite, and a handful of
react-hooks/exhaustive-deps warnings are suppressed where effects intentionally
run once.
MIT — see LICENSE.