2026-04-02
XP and leveling system — shipped
Built and merged the full XP/leveling backend and initial frontend in two stacked PRs (#14, #15).
What shipped
Backend (PR #14):
- BECMI Fighter XP table: levels 1–36, doubles through 1–8, linear 120k/level from 10 onward
- New fields on Members:
xp(cumulative, never decreases),level(auto-computed via beforeChange hook),class(relationship to Classes collection) awardPointsnow awards XP alongside points, returns level-up info for callers- Tier multipliers apply to purchase XP only — event/checkin/quest XP is flat (prevents pay-to-outlevel)
- Classes collection replaces enum — adding/renaming classes is now a data change, no migration needed
- 25 new unit tests for XP table, updated loyalty tests — 68 total passing
- Seed includes XP backfill so test member starts at Level 4
Frontend (PR #15):
- Dashboard redesigned as character sheet layout: portrait left, name/tier/level/XP bar hero section, stat cards demoted below
- XP progress bar using shadcn Progress (Radix) with proper accessibility (aria-valuenow, clamped values)
- Class name displayed from relationship
Design decisions made today
- XP rate = points rate: $1 = 100 XP = 100 points. Same base, same multiplier rules. GDD confirmed this.
- XP backfill at 1x: all historical purchases get retroactive XP at base rate regardless of current tier. GDD calls this the “powerful first moment” — regulars sign up and see Level 3+ immediately.
- Classes renamed: fighter/magic-user/cleric/rogue → warrior/mage/healer/rogue. FF-inspired, universally recognizable. Descriptions are taglines not sentences: “Strength, steel, and glory”
- Classes as collection, not enum: future expansion (subclasses, EQ-style tree) is a data change, not a migration. Worth the extra join.
- Don’t modify auto-generated migrations: use migrate:fresh for local dev. Production runs once in order.
Staging deployment notes
The migration dance on staging was painful — dev mode pushes schema changes without recording them in the migrations table. Had to manually insert/delete migration records and drop columns via SQL. For future reference:
- Check
payload_migrationstable before deploying - If dev mode already pushed columns, drop them + delete the stale migration record before deploying the new one
- Or just wipe staging if there’s no real data
Copilot review — one real bug caught
Copilot found a real bug: the level beforeChange hook would reset level to 1 on any member update that didn’t include xp in the payload (e.g., subscription status change). Fixed by falling back to originalDoc.xp. Also created #16 for the pre-existing race condition on read-modify-write in awardPoints.
Created /gh skill
Global Claude Code skill for GitHub CLI operations — PRs, issues, review comment replies. Key gotcha documented: reply endpoint is pulls/comments/{id}/replies, not nested under the PR number.
/r/outside connection
Realized why the guild concept resonates so strongly: it’s literally the /r/outside MMORPG meme made real. That subreddit (1M+ subscribers) treats real life as an MMO — guilds, quests, XP, loot, character classes. dungeon.club does the same thing but with an actual system behind it.
Reference: https://kotaku.com/if-real-life-was-an-mmo-806521696
Why this matters for virality
- The vocabulary is already established — people who know /r/outside instantly understand what we’re doing
- No onboarding needed for the metaphor — “your bookstore is a quest hub” lands immediately
- The subreddit audience skews toward exactly the kind of people who’d join a gamified bookstore membership
- It writes its own headline: “This bookstore actually turned itself into an MMORPG guild”
How to use it
- Lean into /r/outside language in marketing copy and social posts
- Cross-post or engage with the /r/outside community when we launch
- Frame the store explicitly as a quest hub in a real-world MMO — not as a loyalty program with game aesthetics
Activity feed with centipoints — shipped
guild PR #12. Redesigned the activity feed to show per-item purchase breakdowns with multiplier math. Switched from fractional floating-point points to integer centipoints (1 cent = 1 point, $15.99 = 1,599 pts). Backfilled purchases flagged explicitly instead of inferring from multiplier. Migration fix: replaced auto-generated migration with targeted IF NOT EXISTS column additions to avoid conflicts with the hand-written multi-tenant migration on staging.
NFC kiosk — shipped
guild PR #17. Full self-serve NFC check-in kiosk: kiosk frontend (dark mode, full-screen), check-in now awards 100 XP, API key auth per device via Kiosks collection. WalletMate II reader types hex UID + Enter as HID keyboard → invisible input captures it. States: idle → loading → welcome (character sheet + XP bar) → auto-reset. Duplicate check-ins show live cooldown timer with draining progress bar. Kiosk API key passed via URL param (?key=) so multiple kiosks can connect to one server.
Also: renamed dedup window to cooldown, configurable via CHECKIN_COOLDOWN_MINUTES env var (default 240). Welcome screen extended to 30s for social media photos. Kiosk e2e tests added.
Emporium — dependency fix
store-rebuild e906ca6: hoisted @medusajs/* packages in .npmrc so Vite can resolve admin dependencies. Added missing direct deps (@medusajs/dashboard, @tanstack/react-query).
Deployment fix
guild 3f1f1d8: switched from payload migrate CLI to prodMigrations at init time — works with standalone Next.js output in Docker (no CLI needed).
What’s next
- #13: auto check-in on in-store purchases
- Class selection UI during onboarding
- hi.events integration for event attendance XP