Discord Integration & Selfhosted SSO
Problem
Members need a unified identity across Guild, selfhosted apps (Kavita, Calibre Web, HedgeDoc, Affine, Nextcloud), and Discord. Paid-tier members should get access to selfhosted resources. Guild is the single source of truth for membership.
Why Discord (subculture filter, not bug)
Discord-as-identity is an explicit beachhead-vertical assumption. Per platform-strategy, the platform’s beachhead is gaming/nerd-culture third-spaces — game stores, comic shops, RPG-leaning bookstores, board game cafés, hackerspaces. These shops have Discord-native customer bases. Adopting Discord OAuth as the primary social login filters for the subculture rather than fighting it.
Verticals where this is a poor fit (record stores, plant shops, non-gaming cafés) are out of scope for the beachhead and may be revisited post-PMF.
Architecture
Guild owns membership data. Authentik translates tier into OIDC tokens for selfhosted apps. Discord is a login method, not a source of truth. Marty is a Discord bot — not in the auth chain.
Stripe webhook → Guild updates tier
→ syncMemberToSquare() (POS discount)
→ syncMemberToAuthentik() (selfhosted app access)
Member accesses selfhosted app (Kavita, Calibre Web, etc.)
→ auth.dungeon.club (Authentik)
→ "Log in with Discord" (or email/password via Guild account)
→ Authentik issues OIDC token with tier group claims
→ App grants access based on groups
Phases
Phase 1 — Guild Discord Login ✅
Discord OAuth via Better Auth (no payload-auth plugin).
- Members link Discord from dashboard settings
discordId+discordUsernamestored on Member record- “Log in with Discord” on login page for linked accounts
- Signup remains invite-gated email/password; Discord is linked after
Shipped in: PR #86 (change password), PR #87 (Discord OAuth)
Phase 2 — Authentik SSO for Selfhosted Apps ✅ (POC shipped 2026-04-09)
Authentik deployed at auth.dungeon.club on Cosmos (Proxmox LXC) as the OIDC provider for selfhosted apps. Guild pushes tier data directly to Authentik — no Discord middleman.
POC app shipped: HedgeDoc (write.dungeon.club) — session notes and writing workshops. Live with OIDC via Authentik, gated by Guild Member group. Discord is the social login source (Marty prod app). syncMemberToAuthentik() runs on signup, creates Authentik user + adds to group. All 7 existing members backfilled via script. See 2026-04-09.
How it works:
- Guild pushes member tier to Authentik via API when tier changes
- Authentik groups map to app permissions (e.g.
guild-silver,guild-gold,guild-mithril) - Kavita reads groups from OIDC claims → maps to library access
- Members log into Authentik with Discord (same OAuth app) or email
Guild changes:
syncMemberToAuthentik()inlib/authentik-sync.ts— mirrorssquare-sync.tspattern- Wire into Stripe webhook: after
syncMemberToSquare(), callsyncMemberToAuthentik() - Fire-and-forget with console.warn on failure (same pattern as Square sync)
Authentik setup:
- Container on Cosmos (needs PostgreSQL + Redis)
- Cloudflare Tunnel:
auth.dungeon.club→localhost:9000 - Discord as social login source (same Marty app, add Authentik redirect URI)
- Groups:
guild-free,guild-silver,guild-gold,guild-mithril - One OIDC provider per app
HedgeDoc OIDC config (POC):
- Currently hosted at
hedgedoc.kuroda.cloud(Cosmos, Tailnet only) - Public domain:
write.dungeon.clubvia Cloudflare Tunnel - OIDC provider: Authentik
- Ref: https://docs.hedgedoc.dev/references/config/auth/oidc/
- Members log in with Discord (via Authentik) to edit/create docs
- Permission model: logged-in users can edit, public can view (per-doc setting)
Apps to onboard (in order):
- HedgeDoc (
write.dungeon.club) — session notes, writing workshops (POC) - Kavita (
read.dungeon.club) — free reference shelf (basic rules, SRDs) - Calibre Web (
lib.dungeon.club) — ebook management/downloads - Nextcloud — file storage, member interest
- Affine — Notion alternative (if adopted)
Env vars (Guild):
AUTHENTIK_API_URL— Authentik base URLAUTHENTIK_API_TOKEN— Authentik API token for user/group management
Phase 3 — Discord Role Sync (Marty, optional)
Cosmetic Discord roles showing tier in the server. Not in the auth chain — purely for community visibility.
- Marty assigns Discord roles when tier changes
- Guild calls Marty API alongside other syncs
- If Marty is down, nothing breaks — members still have app access via Authentik
Not a priority. Can ship anytime after Phase 2 or skip entirely.
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-2.
Who Owns What
| System | Responsibility |
|---|---|
| Guild (Payload) | Source of truth for members, tiers, billing. Pushes tier to Square + Authentik. |
| Authentik | SSO for selfhosted apps. Issues OIDC tokens with tier-based group claims. |
| Marty | Discord bot. Cosmetic role sync (optional). Not in the auth chain. |
| Kavita/CWA/HedgeDoc/etc. | OIDC clients of Authentik. |
| Stripe | Payment → Guild webhook → tier update → Square + Authentik sync |
Open Questions
- Authentik: share Guild’s PostgreSQL instance or run separate?
- Authentik: API token scoping — what permissions does Guild need?
- Kavita library structure: which libraries are tier-gated vs. free?
- Tier → library mapping: does Silver get a subset of Gold, or different collections entirely?
- Authentik user creation: push from Guild on signup, or lazy-create on first login?
- What happens when a member downgrades/cancels? Revoke Authentik groups immediately or grace period?