Discord Integration — Auth & Role Sync
Problem
Members need a unified identity across Guild, selfhosted apps (Kavita, CWA, HedgeDoc), and Discord. Currently Guild has email/password auth with no Discord awareness, and selfhosted apps have no SSO.
Architecture
Guild is the source of truth for membership. Discord is the primary member identity. Marty manages Discord roles. Authentik provides SSO for selfhosted apps.
Member signs up / logs in to Guild via Discord (payload-auth)
→ Guild stores discordId on Member record
→ Stripe checkout for tier subscription
→ Stripe webhook → tier update → sync to Square + Discord
Member accesses selfhosted app (Kavita, CWA, HedgeDoc)
→ auth.dungeon.club (Authentik)
→ "Log in with Discord"
→ Authentik maps Discord roles → groups → app permissions
Marty POST /discord/roles/sync
→ removes old tier roles → assigns new tier role
Phases
Phase 1 — Guild Discord Login (payload-auth)
Install @payload-auth/better-auth-plugin with Discord social provider.
- Members sign up / log in to Guild with Discord
discordIdstored on Member record automatically via Better Auth- Signup flow: Discord login → select tier → Stripe checkout
- Existing members: prompt to link Discord on next login
- Stripe billing flow unchanged — just triggered after Discord auth
Guild changes:
- Add payload-auth plugin with Discord provider
- Add
discordId(immutable snowflake, unique, indexed) anddiscordUsernameto Members collection - Update signup flow: Discord login first, then tier selection + Stripe checkout
- Dashboard: show connected Discord identity
Env vars:
DISCORD_CLIENT_ID(reuse Marty’s Discord app)DISCORD_CLIENT_SECRET
Phase 2 — Discord Role Sync (Marty)
When tier changes in Guild, push the matching Discord role via Marty.
Marty changes:
- New endpoint:
POST /discord/roles/sync— receives{discordId, tierSlug, subscriptionStatus} - New endpoint:
POST /discord/roles/remove— strips all tier roles (for cancel) - Bearer token auth via shared API key with Guild
- Role mapping via
DISCORD_ROLE_MAPenv var (JSON:{"bronze":"role_id", ...}) - Bot needs
manage_rolespermission, role positioned above tier roles in server hierarchy
Guild changes:
syncMemberToDiscord()inlib/discord-sync.ts— mirrorssquare-sync.tspattern- Wire into Stripe webhook: after
syncMemberToSquare(), callsyncMemberToDiscord() - Fire-and-forget with console.warn on failure (same as Square sync pattern)
Env vars (Marty):
GUILD_API_KEY— shared secretDISCORD_GUILD_ID— the Dungeon Books serverDISCORD_ROLE_MAP— JSON tier-to-role mapping
Env vars (Guild):
MARTY_API_URLMARTY_API_KEY
Phase 3 — Authentik SSO for Selfhosted Apps
Deploy Authentik (auth.dungeon.club) as the central identity provider for all selfhosted apps.
- Discord as social login source in Authentik
- Authentik maps Discord roles → Authentik groups
- Each app configured as an OIDC provider in Authentik:
- Kavita (
read.dungeon.club) — groups determine library access - CWA (
lib.dungeon.club) — groups determine shelf/download permissions - HedgeDoc (
docs.dungeon.club) — groups determine edit/read access
- Kavita (
- One login experience across all apps
- New apps just register as an OIDC client in Authentik
Infrastructure:
- Authentik container on Cosmos (needs PostgreSQL + Redis)
- Cloudflare Tunnel route:
auth.dungeon.club→localhost:9000 - Consider: Authentik may share Guild’s PostgreSQL or run its own
Phase 4 — Dungeon Arts Nonprofit (future)
Authentik becomes the auth layer for the nonprofit membership too. Same Discord identity, different tier/permissions structure. Groundwork laid in phases 1-3.
Who Owns What
| System | Responsibility |
|---|---|
| Guild | Source of truth for members, tiers, billing. Discord login via payload-auth. |
| Marty | Owns Discord role assignment. Exposes API for Guild to call. |
| Authentik | SSO for selfhosted apps. Discord as identity source. |
| Kavita/CWA/HedgeDoc | OIDC clients of Authentik. |
| Stripe | Payment → Guild webhook → tier update → Square + Discord sync |
Open Questions
- Create Discord roles in server matching tier names (bronze/silver/gold/mithril)
- Position Marty’s bot role above tier roles in Discord server hierarchy
- Add
manage_rolespermission to Marty’s Discord bot - Add OAuth2 redirect URI to Marty’s Discord app in Developer Portal
- payload-auth: does Better Auth store discordId in its own tables or can we map to Members collection fields?
- Authentik: share Guild’s PostgreSQL instance or run separate?
- Authentik: how to map Discord roles → Authentik groups automatically?
- Retry strategy: fire-and-forget for now, add
discordSyncPendingflag later if needed? - Existing email/password members: migration path to Discord login?