Files
member-console/docs/discourse-setup.md
T
cgalo5758 2d71aa8db0 Describe the boot that serves before its dependencies
Seventh section of #141, the docs. docs/production-deployment.md's first
boot section lists the steps in the order start runs them, says what
serves from the moment the listener opens, and describes the two loops
that run beside it: the identity provider's discovery, with sign-in at
503 until a usable document is published and roles not re-derived
before a document with the configured issuer is read, and Temporal,
retried for the life of the process with its log turning to Error after
temporal-connect-timeout. The service list says that Stripe and
Discourse need Temporal. README.md and docs/plan-management.md lose
their description of a bounded retry before the server comes up.

docs/environment-reference.md rewrites the temporal-connect-timeout and
temporal-host rows; docs/stripe.md and docs/discourse-setup.md say why
temporal-host is required; docs/building-an-integration.md documents
NeedsTemporal. docs/fedwiki-setup.md says the console answers the
certificate ask as soon as it serves. docs/identity-provider-setup.md
and internal/auth/README.md replace "refuses to start" with what the
console does, and docs/temporal-authorization-setup.md explains the
token URL. Comments in the FedWiki and Stripe integrations and the
FedWiki workflows README that still described the old boot are
corrected. Every quoted log line is the one the code writes.

No code changes. make test passes with the test stack up, and go test
-race -p 1 over cmd, auth, server, retryloop, workflows and the three
integrations after reset-test-db.sh finds no data race.
2026-10-23 21:07:48 -04:00

7.8 KiB

title, audience, summary
title audience summary
Discourse Integration Setup
operator
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://<console-host>/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           <admin 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    <shared 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). Both are set with the deployment: the settings page shows the URL and offers no control, because the API key is sent to whatever that URL names.

A configured Discourse also requires temporal-host: a workflow reconciles its webhook events, and a Temporal schedule restores what a failed event missed, so start refuses the configuration without it.

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.