Membership & Loyalty Platform
Overview and architecture plan for the Dungeon Books membership and loyalty platform. This is both a production system for Dungeon Books and an R&D prototype for a future product targeting independent bookstores, game shops, cafes, and other third spaces running Square POS.
Context
This project is a custom-built, headless membership platform designed to be portable to other Square shops.
The full ecommerce rebuild (Medusa v2 + Solace storefront) is separate and not as pressing — the current webshop is functional. This platform is scoped independently and ships first.
Goals
- Custom membership and loyalty system
- Build around Square POS as the primary integration (most indie shops already have it)
- Own the loyalty logic — tiers, not just points — loosely coupled to Square
- Attribute in-store purchases to members
- Let members check in via NFC, QR, or phone tap
- Give members a portal to view purchases, events attended, tier status, and profile
- Give staff a dashboard to manage members, view check-ins, and see loyalty status at POS
- Track event attendance and tie it to member profiles
- Learn what works at Dungeon Books, then generalize into a product for other shops
Non-Goals (for now)
- Full ecommerce rebuild (Medusa + online storefront) — separate project, ships later
- Nonprofit revenue routing — not yet incorporated, all revenue goes to the LLC
- Multi-location support — Dungeon Books is one shop; multi-location is a product concern for later
- Mobile app — web-first, responsive; native app is a future product consideration
Architecture
┌─────────────────────────────────────────────────────┐
│ FRONTENDS │
│ │
│ Member Portal Staff Dashboard Kiosk/Check-in │
│ (Next.js) (Payload Admin (tablet web │
│ + custom views) app) │
└────────┬──────────────────┬──────────────┬──────────┘
│ │ │
└──────────┬───────┘──────────────┘
│
┌──────────┴──────────┐
│ Payload CMS │
│ (headless API) │
│ │
│ Members │
│ Tiers & Loyalty │
│ Events/Attendance │
│ Content │
│ Webhook handlers │
└──────────┬──────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌─────┴─────┐ ┌────┴────┐ ┌─────┴─────┐
│ Square │ │Hi.Events│ │ Future: │
│ POS │ │ │ │ Medusa │
│ │ │ tickets │ │ ecomm │
│ purchases │ │ RSVPs │ │ customer │
│ customers │ │ check-in│ │ sync │
│ terminal │ │ │ │ │
└───────────┘ └─────────┘ └───────────┘
Payload CMS — the hub
Payload is the source of truth for member identity and loyalty state. It handles:
- Member profiles — name, email, phone, tier, join date, NFC card UID, linked Square customer ID, linked Hi.Events attendee ID
- Loyalty engine — tier calculation, points/credit accumulation, reward definitions, promotion rules
- Purchase log — synced from Square via webhooks (not the source of truth for transactions, but a denormalized record for the member portal and tier calculation)
- Event attendance — synced from Hi.Events via webhooks
- Content — member-facing pages, announcements, tier benefit descriptions
- Auth — member login for the portal, staff access for the dashboard
- Webhook receivers — ingests events from Square and Hi.Events, updates member state
This is deliberately not Square Loyalty. Square Loyalty is points-based and would lock us (and future product customers) into Square’s model. Our loyalty engine lives in Payload, uses Square only for purchase data and POS-side customer identification.
Square POS — purchase attribution and in-store identity
Square is the cash register. We integrate with it, not build on top of it.
| Square API | What we use it for |
|---|---|
| Customers API | Link Square customer ID to Payload member; look up member at POS |
| Customer Custom Attributes API | Push tier, points, and member status to Square customer profile (see below) |
| Orders API / Webhooks | Receive purchase events → update member purchase history and loyalty |
| Terminal API | Custom workflows: member check-in screen, upsell prompts, loyalty status display |
| Catalog API | Read-only — product names and categories for purchase log context |
We do NOT use:
- Square Loyalty API (we own loyalty logic)
- Square Subscriptions API (membership billing lives in our system, likely Stripe)
- Square customer notes field (may be used by staff for other purposes; not safe to overwrite)
Square customer sync — custom attributes
Design decision: Use Square Custom Attributes (not customer notes) to push member data to the POS. Custom attributes are namespaced to the app, don’t conflict with staff-written notes, and work cleanly for other shops that may already use notes. This is the portable approach for the future product.
Custom attribute definitions to create:
| Key | Display name | Type | Visibility | Example value |
|---|---|---|---|---|
guild_tier | Guild Tier | STRING | VISIBILITY_READ_WRITE_VALUES | "Gold" |
guild_points | Guild Points | STRING | VISIBILITY_READ_WRITE_VALUES | "1,500 ($15.00)" |
guild_status | Membership Status | STRING | VISIBILITY_READ_WRITE_VALUES | "active" |
guild_member_id | Guild Member ID | STRING | VISIBILITY_READ_ONLY | "42" |
Sync triggers:
- After loyalty points awarded (purchase webhook)
- After points redeemed
- After Stripe subscription change (tier upgrade/downgrade/cancel)
Implementation approach:
- On app startup or via script: create custom attribute definitions via
POST /v2/customers/custom-attribute-definitions - After each trigger event: upsert attributes via
POST /v2/customers/{customer_id}/custom-attributes/{key} - Staff sees tier, points, and status on the Square customer profile at POS — no need to check Payload
Webhook flow:
Square POS sale → order.completed webhook → Payload webhook handler
→ match Square customer_id to Payload member
→ log purchase
→ recalculate loyalty (points, tier progression)
→ push updated tier/points back to Square via custom attributes
Hi.Events — event ticketing and attendance
Hi.Events handles what it’s good at: selling tickets, managing RSVPs, and QR code check-in at the door. We sync attendance data back to Payload.
| Concern | How |
|---|---|
| Ticket payments | Stripe Connect (Hi.Events native) — separate from Square |
| Event creation | Hi.Events admin |
| Event pages | Hi.Events hosted or embedded |
| Attendance tracking | Hi.Events QR check-in → webhook → Payload |
| Member attribution | Match attendee email to Payload member |
Future: Medusa ecommerce integration
When the online store rebuild ships, Medusa will have its own customer records. At that point:
- Medusa customer can be linked to Payload member (store
medusa_customer_idon member profile) - Online purchases flow through Medusa → webhook → Payload → same loyalty calculation as Square purchases
- A member’s portal shows both in-store (Square) and online (Medusa) purchase history
This is additive. The membership platform does not depend on Medusa.
Member Identity
A member exists in up to three systems. Payload is the join.
| System | ID | When created |
|---|---|---|
| Payload | member record (primary) | When someone signs up for membership |
| Square | customer_id | When they first buy in-store, or when we create/link during signup |
| Hi.Events | attendee record | When they first buy a ticket or RSVP |
Linking is done by email as the common key, with explicit ID references stored on the Payload member record:
Member (Payload collection)
├── email (unique, indexed)
├── name
├── phone
├── tier (bronze | silver | gold | mithril)
├── loyalty_points (never expire)
├── join_date
├── nfc_card_uid (nullable)
├── square_customer_id (nullable)
├── hi_events_attendee_id (nullable)
├── medusa_customer_id (nullable, future)
├── purchases[] (synced from Square/Medusa)
├── events_attended[] (synced from Hi.Events)
└── membership_subscription (billing status, plan, renewal date)
Online-only members
Some members may never visit the physical store. They can:
- Sign up via the member portal
- Earn loyalty from online purchases (once Medusa ships)
- Attend virtual events (if offered)
- Have no Square customer ID or NFC card — that’s fine
Walk-in customers without membership
A Square customer who buys something in-store but isn’t a member just stays in Square. No Payload record is created. If they later sign up for membership, we link their Square history retroactively.
Tiers & Loyalty
Inspired by credit card models: you pick and pay for a tier (like choosing a card), and you earn loyalty points by engaging with the shop (like earning miles). Two separate systems that interact.
Design principles
- Points never expire. If a member takes a year off and comes back, their balance is there.
- No dark patterns. No “use it or lose it” urgency, no expiration emails, no loss aversion hooks. We want people to redeem — redemption means they’re in the shop.
- Redemption should feel like celebration, not transaction. End-of-year points party, exclusive redemption events, community milestones. Spending points is a good time, not a checkout line discount.
- No breakage by design. Credit card companies profit from unspent points. We don’t want that. High redemption rate = healthy program.
Tiers (what you pay for)
Members choose and pay for their tier. Billing via Stripe (see Membership Billing section). Benefits are battle-tested, keeping as-is.
Iron is the internal designation for non-member customers tracked in Square’s customer directory. They have no Payload member record.
Bronze — $10/mo
| Benefit | Details |
|---|---|
| Welcome gift | Dungeon Books member sticker (pickup) |
| Store discount | None |
| Event discount | 10% off all events (online tickets only) |
| Event access | Early access to RSVP for special events |
| Birthday perk | $10 store credit during birthday month |
| Loyalty multiplier | 1x |
Silver — $25/mo
| Benefit | Details |
|---|---|
| Welcome gift | Dungeon Books member sticker (pickup) |
| Yearly gift | Dungeon Books member tote bag (Dec 15) |
| Store discount | 10% off all store purchases |
| Event discount | 10% off all events (online tickets only) |
| Guest sharing | Share your discount with 1 additional guest per event |
| Event access | Early access to RSVP for special events |
| Birthday perk | $10 store credit during birthday month |
| Free ARC | 1 free advance reader copy per month (in-store pickup only) |
| Loyalty multiplier | 1.5x |
Gold — $50/mo
| Benefit | Details |
|---|---|
| Welcome gift | Dungeon Books member sticker (pickup) |
| Yearly gift | Dungeon Books member tote bag (Dec 15) |
| Store discount | 20% off all store purchases |
| Event discount | 15% off all events (online tickets only) |
| Guest sharing | Share your discount with 1 additional guest per event |
| Event access | Early access to RSVP for special events |
| Birthday perk | $10 store credit during birthday month |
| Free ARC | 1 free advance reader copy per month (in-store pickup only) |
| Loyalty multiplier | 2x |
Mithril — $100/mo
| Benefit | Details |
|---|---|
| Welcome gift | Dungeon Books member sticker (pickup) |
| Yearly gift | Dungeon Books member tote bag & member t-shirt (pickup or delivery, shipping included) |
| Curated items | Hand-picked items from the collection, every 3 months (pickup or delivery, shipping included) |
| Store discount | 25% off all store purchases |
| Event discount | 50% off all events (online tickets only) |
| Guest sharing | Share your discount with 1 additional guest per event |
| Event access | Early access to RSVP for special events |
| Birthday perk | Hand-picked book by the Guildmaster team, just for you (pickup only) |
| Free ARC | 1 free advance reader copy per month (in-store pickup only) |
| Private event | Use of the bookstore for a private event once per year (birthday, book club, engagement party, etc.) |
| Loyalty multiplier | 3x |
One-time contributions
Not a tier. Members and non-members can make a one-time contribution of any amount to support the shop.
Loyalty points (what you earn)
Points are earned through engagement with the shop. The earn rate scales with tier.
Design decision: points as universal loyalty currency. Points are the internal currency stored in Payload. They are not automatically converted to store credit. Members accumulate points and choose how to redeem them — store credit is one option among many. This keeps the system flexible (double-point promos, cross-platform redemptions, non-monetary rewards) and avoids tax implications until redemption. The member portal always shows the dollar-equivalent alongside the point balance (e.g. “1,500 points ($15.00 store credit value)”) so members understand the value without needing to do math.
Earning:
| Action | Base rate | Scaled by tier multiplier? |
|---|---|---|
| In-store purchase (per $1 spent) | 1 point | Yes |
| Online purchase (per $1, future) | 1 point | Yes |
| Event attendance | TBD (flat bonus) | Yes |
| Check-in / visit | TBD (flat bonus) | Possibly |
Tier multipliers (example, tunable):
| Tier | Multiplier |
|---|---|
| Bronze | 1x |
| Silver | 1.5x |
| Gold | 2x |
| Mithril | 3x |
A Mithril member spending $50 earns 150 points. A Bronze member earns 50. The multiplier is the core incentive to upgrade tiers.
Bonus events (staff-configured, not automated dark patterns):
- Double point days tied to real occasions (store anniversary, Free RPG Day, new releases)
- Community milestones — “the shop collectively hit 100,000 points this quarter, everyone gets a bonus”
- Category bonuses — 2x on RPG products this month, 2x during an author visit
These are configured by staff as promotions in Payload, not hardcoded. They should feel like gifts and celebrations, not manufactured urgency.
Redeeming:
Points are a universal currency redeemable across multiple paths. Store credit is the primary path, but not the only one.
| Reward type | Example | How it works |
|---|---|---|
| Store credit | 100 points = $1 | Tracked in Payload, applied as discount at POS by staff |
| Experience access | 500 points = priority seat at a special event | Unlocks ticket in Hi.Events |
| Exclusive items | 2,000 points = limited edition merch | Staff fulfills, logged in Payload |
| Community events | End-of-year points party, redemption pop-ups | Points are the entry ticket |
| Digital perks | 1,000 points = Discord role/flair | Automated via bot integration |
Redemption values are configurable per shop (important for the future product). The engine tracks earn and redeem as a ledger — every point transaction is logged with source, amount, and timestamp. Tax implications only arise at the moment of redemption for monetary-value rewards (store credit), not when points are earned or held.
How tiers and loyalty interact
TIER (paid monthly) LOYALTY (earned by engagement)
├── determines benefits ├── accumulated from purchases,
├── determines earn multiplier events, visits
├── member chooses their tier ├── points never expire
│ ├── redeemed for rewards
│ └── higher tiers earn faster
│
└── upgrading tier = higher └── points incentivize spending
multiplier = faster at your shop instead of
point accumulation elsewhere
The tier fee pays for itself through the multiplier and exclusive access. The loyalty points keep them coming back. Neither system uses expiration, artificial scarcity, or loss aversion to retain members.
Check-in & Identification
How a member is recognized when they walk in or buy something.
At purchase (Square POS)
| Method | Flow | Complexity |
|---|---|---|
| Phone number lookup | Staff asks for phone → Square POS customer search → purchase attributed | None — built into Square |
| Square loyalty tap | Member taps phone (Apple/Google Wallet) on reader → Square identifies customer | Low — requires Square Loyalty enabled, even if we don’t use their points system |
| Terminal API prompt | Terminal shows “Member?” screen → member enters phone/scans QR → Payload API lookup → Square customer linked | Medium — custom Terminal API workflow |
At the door (non-purchase check-in)
| Method | Flow | Complexity |
|---|---|---|
| NFC card tap | Member taps physical NFC card on tablet → tablet reads card UID → Payload API lookup → check-in logged | Medium — need NFC-enabled tablet + small web app |
| QR code scan | Member shows QR from portal on phone → staff scans or kiosk reads → Payload API lookup | Low-medium |
| Self-serve kiosk | Tablet at door → member enters phone/email or taps NFC → welcome screen, loyalty status shown | Medium |
NFC cards
Issue physical NFC cards (NTAG215/216, ~$0.30 each) with a unique ID written to the chip. The ID maps to the Payload member record. Cards can be branded with the adventurers guild aesthetic.
For phone-based NFC: Apple Wallet and Google Wallet passes can carry a member ID. Libraries exist for generating these (passkit-generator for Apple, Google Wallet API). The pass could display tier, points, and a barcode/QR fallback.
Member Portal
A web app (Next.js) authenticated against Payload. What the member sees:
- Profile — name, tier, loyalty points, membership status
- Purchase history — in-store purchases (from Square sync), online purchases (from Medusa, future)
- Events — past events attended, upcoming RSVPs
- Rewards — available rewards, redemption history
- Membership — plan details, billing, renewal date
- Digital card — QR code and/or Apple/Google Wallet pass download
Staff dashboard
Payload’s admin UI, extended with custom views:
- Member management — search, view, edit members; manual tier overrides
- Check-in log — who’s in the shop right now, recent check-ins
- Loyalty overview — tier distribution, engagement metrics
- Event attendance — per-event attendee lists, cross-referenced with members
Membership Billing
Membership itself needs a recurring payment. Options:
| Option | Pros | Cons |
|---|---|---|
| Stripe (direct) | Hi.Events already uses Stripe; one payment processor for all non-POS revenue; Stripe Billing is battle-tested | Another processor alongside Square |
| Square Subscriptions | Single processor for in-store + membership | Ties the product to Square; less flexible billing logic |
| Payload + Stripe | Payload manages plans/status, Stripe handles billing, webhooks update Payload | Most flexible, most portable for a product |
Recommendation: Payload + Stripe. Keeps billing decoupled from Square (important for product portability). Stripe is already in the stack via Hi.Events. Square stays focused on what it’s best at: in-store POS.
Tech Stack Summary
| Component | Technology | Purpose |
|---|---|---|
| Member identity & loyalty engine | Payload CMS 3.x | Source of truth for members, tiers, loyalty rules |
| Member portal | Next.js (App Router) | Member-facing web app |
| Staff dashboard | Payload Admin UI + custom views | Member management, check-in log, metrics |
| Check-in app | Web app (tablet) | NFC/QR reader for door check-in |
| In-store POS | Square | Purchase attribution, customer lookup |
| Event ticketing | Hi.Events (self-hosted) | Tickets, RSVPs, QR check-in at events |
| Membership billing | Stripe (via Payload) | Recurring membership payments |
| Database | PostgreSQL | Payload data store |
| Hosting | Railway | All services |
| Digital wallet passes | Apple Wallet / Google Wallet APIs | Member cards on phone |
Integration Map
Square POS ──webhook──→ Payload (log purchase, update loyalty)
Hi.Events ──webhook──→ Payload (log attendance, update loyalty)
Stripe ──webhook──→ Payload (membership billing status)
Payload ──API call──→ Square (create/update customer, push tier to notes)
Payload ──API call──→ Hi.Events (future: auto-create attendee for member)
NFC reader ──HTTP──→ Payload API (check-in lookup)
Member portal ──API──→ Payload (read profile, purchases, events)
What We Build vs. What We Buy
| Concern | Build or buy | Notes |
|---|---|---|
| Member profiles & auth | Build (Payload collections) | Core of the platform |
| Loyalty engine (tiers, rules) | Build (Payload custom module) | Product differentiator — must be ours |
| Purchase sync from Square | Build (webhook handler) | Straightforward — Square webhooks are well-documented |
| Event management | Buy (Hi.Events) | Self-hosted, has API and webhooks |
| Membership billing | Buy (Stripe Billing) | No reason to build payment processing |
| NFC cards | Buy (blank NTAG cards + USB reader) | Write member ID to card, ~$0.30/card |
| Digital wallet passes | Build (Apple/Google Wallet APIs) | Libraries exist, moderate effort |
| Member portal | Build (Next.js + Payload API) | Custom UI, moderate effort |
| Staff dashboard | Build (Payload admin extensions) | Payload’s admin is extensible, moderate effort |
Open Questions
Resolved
Tier thresholds— Tiers are purchased (25/100 per month), not earned.Membership pricing— Bronze 25/mo, Gold 100/mo.POS behavior— Staff dashboard (Payload admin) as primary tool + push tier/status to Square customer notes for visibility inside Square. Terminal API custom workflow on the roadmap for v2.Retroactive history— Yes, backfill Square purchase history when a walk-in becomes a member. Configurable per shop (backfill: { enabled: true, max_months: null, rate_multiplier: 1.0 }). Strong onboarding moment.Multi-shop architecture— Multi-tenant shared database with Organization → Shop → Member hierarchy. Addorganizationandshopfields to every collection from day one, hardcoded to Script Wizards LLC / Dungeon Books. Supports multi-location and future cross-org neighborhood networks.Earn rates and redemption— 1 point per 1 store credit. Generous starting point, tunable. Event attendance and check-in bonuses TBD.Tier-specific benefits— Battle-tested, keeping as-is (see Tiers section above). Loyalty points are a new layer on top.
Remaining
- Event attendance and check-in point values — Launch with purchase-based earning only. Add event/check-in bonuses post-launch (starting point: ~25 points per event, ~5 per check-in). Not blocking.
- Discount application at POS — Manual for now (staff applies in Square). Square does not expose an API for automatically creating or updating discounts — this is a known platform limitation. No automated path exists. Staff applies manually; the Payload dashboard and Square customer notes tell them the rate.
- Member migration — Need to export existing members, tiers, join dates, billing. Research before cutover, not blocking for build.
- Buying points / point boosts — Analogous to video game season pass point boosts. Could translate to buying gift cards that grant loyalty points (e.g., $25 = 2,500 points, with tier multiplier applied). Blurs the line between gift cards and loyalty in an interesting way. Future consideration — not MVP.
Relationship to Other Projects
- Store rebuild (Medusa + Solace storefront) — ships separately, later. When it does, Medusa customers sync to Payload members. The membership platform doesn’t depend on it.
- Webshop (current, Square-backed) — unaffected. Membership platform is a new, parallel system.
- Marty (AI chatbot) — could eventually look up member info for personalized recommendations via Payload API.
- Future product — architecture should be generalizable to other indie shops.
Related
- membership-program — brand concept, tier names, visual identity
- square-integration — existing Square API usage
- store-rebuild — future ecommerce architecture (Medusa + Payload)
- roadmap — overall project timeline