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
  • discordId stored 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) and discordUsername to 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_MAP env var (JSON: {"bronze":"role_id", ...})
  • Bot needs manage_roles permission, role positioned above tier roles in server hierarchy

Guild changes:

  • syncMemberToDiscord() in lib/discord-sync.ts — mirrors square-sync.ts pattern
  • Wire into Stripe webhook: after syncMemberToSquare(), call syncMemberToDiscord()
  • Fire-and-forget with console.warn on failure (same as Square sync pattern)

Env vars (Marty):

  • GUILD_API_KEY — shared secret
  • DISCORD_GUILD_ID — the Dungeon Books server
  • DISCORD_ROLE_MAP — JSON tier-to-role mapping

Env vars (Guild):

  • MARTY_API_URL
  • MARTY_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
  • 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.clublocalhost: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

SystemResponsibility
GuildSource of truth for members, tiers, billing. Discord login via payload-auth.
MartyOwns Discord role assignment. Exposes API for Guild to call.
AuthentikSSO for selfhosted apps. Discord as identity source.
Kavita/CWA/HedgeDocOIDC clients of Authentik.
StripePayment → 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_roles permission 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 discordSyncPending flag later if needed?
  • Existing email/password members: migration path to Discord login?