Invitations
Invite codes — creating, checking, redeeming
Shareable codes for playtest access and friend invites. Codes look like BH-7KQ2-XVNM — the alphabet deliberately drops 0/O/1/I so codes survive being read aloud or scribbled down.
Lifecycle
# Create (signed in) — defaults: 5 uses, 30 days
curl -X POST http://localhost:3001/api/invites \
-H 'Content-Type: application/json' -b cookies.txt \
-d '{"maxUses": 10, "expiresInDays": 14}'
# → 201 {"code": "BH-7KQ2-XVNM", "maxUses": 10, "uses": 0, ...}
# Check (public — for the "enter your code" screen)
curl http://localhost:3001/api/invites/BH-7KQ2-XVNM
# → {"code": "...", "valid": true, "remainingUses": 10, "expiresAt": "..."}
# Redeem (signed in)
curl -X POST http://localhost:3001/api/invites/BH-7KQ2-XVNM/redeem -b cookies.txt
# → 200 {"ok": true, "code": "BH-7KQ2-XVNM"}
# List your own invites (signed in)
curl http://localhost:3001/api/invites -b cookies.txtRules
| Rule | Response when violated |
|---|---|
| Must be signed in to create/redeem | 401 |
| Unknown code | 404 |
| Can't redeem your own invite | 400 |
| One redemption per user per invite | 409 |
| Expired or all uses consumed | 410 |
Race safety
Redemption runs in a transaction: a conditional increment (uses = uses + 1 WHERE uses < maxUses AND expiresAt > now()) followed by the redemption insert. Concurrent redeems of the last slot can't oversell — one wins, the other gets 410. The unique index on (inviteId, userId) catches double-redeems and rolls the increment back.
What redemption means
Right now a redemption is a recorded fact (invite_redemptions) plus a Discord ping — gating is up to product logic later (e.g. "playtest builds require a redeemed invite", "invitees get a founder badge"). The data model already supports both.