2026-03-27
guild — picking up next work
Reviewed git history and roadmap. Member portal (login, dashboard, activity, tiers) and Stripe billing portal are done (PRs #3 and #4).
Notes from Carrie
- Center for Fiction (Brooklyn) — nonprofit with a bookstore and cafe inside, doing what we want to do. Events page: https://centerforfiction.org/events/
Marty
- Merged MTG embed stuff
guild — Square customer linking (PR #5)
Built auto-linking of Square customers on Guild signup. When a new member subscribes via Stripe, the webhook now:
- Searches Square by email (then phone) for an existing customer
- If matched — links them and backfills loyalty points from past orders (base 1x)
- If no match — creates a new Square customer, flags for staff review
Key decisions:
- Backfill at 1x regardless of tier — tier multiplier only applies going forward
- All-time history — every past purchase counts
- Orders search, not payments list — Square payments.list doesn’t reliably return by customer ID in sandbox
- Runs async in the Stripe webhook, not the checkout route (keeps signup fast, only links confirmed subscribers)
Tested all 3 scenarios locally (match + backfill, match + no purchases, no match + create new).
PR review deferred items (PR #5)
- Unit tests for
square-customer.ts— tracked in #6 - Fire-and-forget webhook — linking runs inline, could hit Stripe’s 30s timeout with huge order histories. Fine for a bookshop, revisit with job queue
- Multi-shop backfill — only backfills from first shop in org. Single shop for now
- Batch dedup — per-order DB lookup in backfill loop. Fine at current scale (~50-100 orders)
Future: job queue
When async work stacks up (Hi.Events webhooks, achievement calculations, email notifications), bring in BullMQ + Redis. Redis will already be in the stack for the store rebuild (Medusa). Not worth adding for one webhook handler — the signal is Stripe retries or needing to decouple work from request/response.
v1 alpha gap analysis
Reviewed full codebase for launch readiness. ~75% there. Key gaps:
Must fix (tonight):
Redeem endpoint auth bug — any member can redeem anyone’s points✓No email at all — need Resend + welcome email + password reset✓No password reset flow✓
guild — email + password reset + redeem fix (PR #7)
Knocked out all three “must fix” items in one PR:
- Resend email —
@payloadcms/email-resendadapter, domain verified, DKIM/SPF passing - Password reset — forgot password page → email with token → reset page. Payload auto-logs in after reset (nice UX). Custom email template points to frontend, not admin panel
- Welcome email — fires on
customer.subscription.createdwebhook, includes tier name. Carrie confirmed receipt - Redeem auth — added
user.collection !== 'users'check, members now get 403
Railway deployment plan
Deploying to Railway for alpha with Panat + Carrie as the only users. Goal: use real-ish data without spending real money or touching other customers’ data.
Integration strategy per service
Square — prod, filtered
- Use real production token so we get real purchase data flowing through loyalty
- Add a customer ID filter to the Square webhook — only process payments tied to Panat’s Square customer ID, skip everything else
- Backfill on signup pulls real order history, new purchases award points in real-time
- The webhook still receives all payment events from the shop; the filter just skips processing. No data is written for other customers
- Remove the filter when ready to go live for real members
Stripe — test mode
- Keep test mode. Subscriptions, billing portal, webhooks all work identically to prod
- Sign up with test cards (
4242...), get real member accounts - Only difference from prod: no actual charges
- Flip to live keys when ready to charge real dues
Hi.Events — real test event (when built)
- Create a real event in Hi.Events (e.g. “Guild Test Night”)
- RSVP/check in to trigger the webhook with real payload format
- Better than a sandbox — tests the actual integration path
- Most event platforms don’t have staging environments anyway
Resend — already live
- Domain verified, DKIM/SPF passing. Emails are cheap. No reason to sandbox this.
The gap to accept
Member accounts will have a real Square customer link but a test Stripe subscription. That’s fine — they’re independent systems. Points come from Square purchases, billing comes from Stripe. They don’t need to talk to each other.
Env vars needed on Railway
DATABASE_URL— Railway PostgresPAYLOAD_SECRET— strong random value (not the dev one)SQUARE_ACCESS_TOKEN— prod tokenSQUARE_WEBHOOK_SIGNATURE_KEY— new, for Railway URLSQUARE_WEBHOOK_URL— Railway domainSQUARE_ALLOWED_CUSTOMER_IDS— Panat’s Square customer ID (new, for filtering)STRIPE_SECRET_KEY— test mode keySTRIPE_WEBHOOK_SECRET— new, for Railway URLRESEND_API_KEY— same as devNEXT_PUBLIC_SERVER_URL— Railway domain
Railway deploy — Docker build fixes
First successful Railway build after a painful round of Docker issues. All common Payload CMS + Next.js + Docker problems:
-
Prerender failures — any page calling
getPayload()orgetMember()crashes duringnext buildbecause env vars aren’t available at Docker build time. Fix:export const dynamic = 'force-dynamic'on every page that touches Payload. This is the standard community solution (confirmed on Reddit, Payload Discord). Added to: login, forgot-password, signup, signup/success, dashboard layout. -
Missing
output: 'standalone'— Dockerfile expects.next/standalonebut Next.js doesn’t produce it by default. Addedoutput: 'standalone'tonext.config.ts. -
No
/app/publicdirectory — Dockerfile tried toCOPY --from=builder /app/public ./publicbut we don’t have a public dir. Removed the line (Dockerfile even had a comment saying to do this). -
No
.dockerignore—COPY . .was shipping.env,.git,node_modulesinto the image. Created.dockerignore. -
sharpin standalone — standalone mode doesn’t always bundle native modules. SetNEXT_SHARP_PATH=/app/node_modules/sharpin the runner stage. -
Non-null assertion lint warnings — replaced
(await getMember())!with proper null check + redirect in 3 dashboard pages. Unrelated to Docker but cleaned up in the same pass.
Live at: https://dungeon.club (Railway, guild-production-1747.up.railway.app)
Prod setup completed
- Railway deploy working (Dockerfile, standalone output, force-dynamic pages)
- Postgres migrated and seeded (org, shop, 4 tiers with Stripe price IDs)
- Admin user created, Square location ID set
- Stripe webhook registered and verified (
dungeon.club/api/webhooks/stripe) - Square webhook registered (
dungeon.club/api/webhooks/square) — same endpoint as local, no customer filter yet - First prod signup tested — welcome email delivered via Resend
- Gotcha: Stripe webhook secret was set wrong (had
VAR_NAME=valueas the value), caused invalid signature errors
E.164 phone fix
Square customer linking was failing because phone numbers were stored as raw digits (2015550123) but Square requires E.164 format (+12015550123). Added toE164() helper in square-customer.ts. Redeployed, created fresh test member — Square customer linked successfully.
Profile completion + integrations vision
Carrie pitched Hardcover.app integration — when a member buys a book in-store (tracked via Square), they can add it to their Hardcover reading profile. Guild becomes the identity layer tying the whole ecosystem together.
Planned post-signup profile flow:
- After signup success, optional “Complete your profile” page (skippable)
- Birthday (already a Members field, used for birthday month perks)
- Address (needed for Mithril tier shipping — curated items quarterly)
- Link Hardcover.app (auto-track book purchases to reading profile)
- Link Discord (member role sync)
- Accessible anytime from dashboard settings
Design principle: don’t add friction to signup. Keep the core flow fast (name, email, phone, password → payment), let members fill in details later when they see the value.
Remaining for v1 alpha
Tomorrow (2026-03-28):
- Kiosk app (NFC cards arrived, using Flipper Zero as reader until reader ships)
- Test Square purchases flowing through loyalty engine on prod
Later:
- Profile completion flow (birthday, address, integrations)
- Hardcover.app integration
- Discord role sync
- Square webhook customer filter (if needed — skipping for now since it’s just us)
- Staff redemption UI (currently API-only)
- Staff dashboard (check-ins, recent activity)
- E2E Playwright tests are stale
- Hi.Events webhook
- Achievement system