--- title: "Discourse Integration Setup" audience: [operator] summary: "Connecting the member console to a Discourse forum: forum-side prerequisites, linkage-mode selection (email / oidc / discourseconnect), group mapping, webhooks, and the settings that must stay off." --- # Discourse integration — operator setup The Discourse integration delivers the org-held `discourse_posting` entitlement as membership in Discourse groups you choose. The console computes who should be in each mapped group (everyone with an active membership in any org that currently holds the entitlement), converges the forum to that set on a periodic sweep, and corrects drift. What those groups *gate* (category permissions, posting rights) stays entirely in your Discourse admin — the console manages membership, never permissions. Works against any Discourse instance. Wiki.cafe's deployment is the tested reference, not an assumption. ## 1. Forum-side prerequisites 1. **A group to manage.** Create a *manually-managed* (non-automatic) group per mapped entitlement (e.g. `members`). Automatic groups (`trust_level_*`, `staff`, ...) are refused at mapping time — Discourse computes their membership itself. **Treat mapped groups as console-owned:** membership changes made by forum admins are corrected on the next sweep (and logged console-side). 2. **An admin API key.** Discourse admin → API → New API Key (global key; note the key and the acting username, usually `system`). Rate-limit note: Discourse's admin API budget (default 60 requests/min) is one **site-wide** bucket shared with human admins. The console caps itself well below it (30/min) and backs off on 429. 3. **(Recommended) A webhook.** Admin → API → Webhooks: payload URL `https:///webhooks/discourse`, a shared secret, events **User** (created) and **Group User** (added/removed). Webhooks only reduce latency — a missed event costs at most one sweep interval. 4. **Never enable `discourse_connect_overrides_groups`.** It full-replaces a user's non-automatic groups at every login, which fights any external group management (this integration's and your admins' alike). ## 2. Choose a linkage mode The console must map its people to forum users. `discourse-linkage-mode` selects how: | mode | forum-side requirement | linkage guarantee | |------|------------------------|-------------------| | `email` | none | verified primary email = forum primary email (exact, case-insensitive). Fragile if emails drift. | | `oidc` (default) | enable the bundled OpenID Connect plugin against the same IdP the console uses (`openid_connect_enabled` + discovery URL + client credentials) | deterministic: forum association ↔ OIDC subject. Existing forum accounts self-link at first SSO login (`match_by_email`, default on). Additive — local logins keep working. | | `discourseconnect` | DiscourseConnect SSO with the console's IdP as provider | deterministic via `external_id`. **Warning: exclusive** — enabling DiscourseConnect disables local and all other login methods for every forum user, and takes over existing accounts by email match. A forum-wide auth decision, not just integration plumbing. | Members without a resolvable forum account appear on the operator page as **entitled but unlinked** — a normal state, not an error. In `oidc` mode they link automatically seconds after their first forum SSO login (via the `user_created` webhook) or at the next sweep. `discourse-auto-create-users=true` additionally creates forum accounts (active, approved, identity-bound in `oidc` mode) for entitled persons who have none. Leave it off unless your deployment wants forum accounts to exist before first visit. Not supported in `discourseconnect` mode. ## 3. Console configuration ``` discourse-base-url https://forum.example.com (required) discourse-api-key (required, secret; -file variant available) discourse-api-username system (default) discourse-linkage-mode oidc (default; email | oidc | discourseconnect) discourse-auto-create-users false (default) discourse-webhook-secret (secret, optional; empty = webhook endpoint unmounted) discourse-sync-interval 15m (default; sweep cadence) ``` `discourse-base-url` and `discourse-api-key` must be set together (or both left empty: the integration stays dormant — registered, migrations applied, no forum calls, and the operator page says so). ## 4. Map entitlements to groups Operator panel → Integrations → Discourse: - **Map group**: choose the entitlement (`discourse_posting`) and enter the forum group's name. The console validates against the forum (group must exist and not be automatic) and stores the mapping; the group is now managed and converges on the next sweep. - **Remove mapping**: stops management. Forum-side membership is left as-is — removal never bulk-kicks members. - The page also shows the last sweep's outcome, entitled-but-unlinked members, and any **link conflicts** (one forum account claimed by two identities — the console freezes the link and never reassigns; resolve forum-side or in member data, then the next sweep relinks). ## 5. Test stack / development Two forum options exist; unit/DB tests always use the fake. **Fake (default — fast, no containers).** An in-process fake implementing the consumed API surface lives at `internal/integrations/discourse/discoursetest`: ``` go run ./internal/integrations/discourse/discoursetest/fakeserver \ -addr 127.0.0.1:9457 -api-key test-key -seed-group walkthrough-members cd test && go run .. start --config mc-config.yaml \ --discourse-base-url http://127.0.0.1:9457 --discourse-api-key test-key ``` **Live Discourse (opt-in compose profile).** A real Discourse — the official `discourse/discourse` image with `discourse/postgres` + redis sidecars, mirroring the Co-op Cloud recipe's topology — runs behind the `discourse` profile so the default stack stays fast (~1.5 GB image, minutes-long first boot): ``` cd test && docker compose --profile discourse up -d # discourse-seed migrates the DB, creates admin (admin@example.com / # discourse-admin-pw1), enables the bundled OIDC authenticator, creates the # walkthrough-members group, registers a webhook pointed at the app on the # host (secret test-only-webhook-secret; the discourse service maps # member-console.localhost to the docker host-gateway), and writes the # master API key to testdata/discourse/api-key. Then, per test/.env's # commented lines: # MC_DISCOURSE_BASE_URL=http://localhost:${DISCOURSE_PORT} # MC_DISCOURSE_API_KEY_FILE=testdata/discourse/api-key # MC_DISCOURSE_WEBHOOK_SECRET=test-only-webhook-secret go run .. start --config mc-config.yaml ``` Re-running the seed (any `--profile discourse up`) re-mints the API key — restart the app afterwards so it reads the fresh key file. `test/seed/discourse/seed-discourse.sh` seeds an org conferred `discourse_posting` so the sweep has someone to deliver. The browser walkthrough (`test/e2e/operator-walkthroughs/discourse_test.go`) exercises the mapping surface end-to-end against either forum (both seed the `walkthrough-members` group) and skips when Discourse is unconfigured. Browser SSO into the live forum additionally needs a `discourse` client in the Keycloak realm — not seeded yet; the OIDC authenticator is enabled for API-level identity binding (`external_ids`) regardless.