roots/discourse-github-sponsors: The Reconciliation Engine Behind Sponsor-Only Communities

A Discourse plugin that syncs GitHub Sponsors into groups, badges, and Discord access, then keeps everything honest with ongoing reconciliation and audit logs.

8 min read • View on GitHub • More from roots

A clerk-like control room with three ledger books on a white desk, each representing GitHub sponsors, Discourse members, and Discord access. Thin threads connect the ledgers while a stamped card marked active sits beside a revoked stack, showing that community access is continuously reconciled rather than granted once.
Sponsor access is treated like live state. The plugin keeps three systems aligned, and it can revoke as deliberately as it grants.
Key Takeaways

When sponsorship becomes state

Most sponsor integrations stop at gratitude. They post a badge, maybe send an email, and call it done. This plugin does something harder: it treats sponsorship like live state that can drift, expire, or be repaired.

The real job is reconciliation. Each pass confirms who still belongs, who no longer does, and which access paths should change.

The trust chain: GitHub, Discourse, Discord

The interesting part is not that GitHub Sponsors exists, or that Discourse has groups. It is that the plugin refuses to assume they know the same person. It uses Discourse’s linked-account table as the bridge, then only extends access when that bridge is present and current.

A Discourse plugin that integrates GitHub Sponsors with your Discourse forum, automatically syncing sponsor status and awarding badges to active sponsors.

Roots, Project Owner/Organization · README.md at main · roots/discourse-github-sponsors

That makes the system safer than a simple webhook. A sponsor can be active in GitHub, linked in Discourse, and eligible for Discord, but each step still has to prove itself.

How the sync engine actually reconciles

The heart of the repo lives in lib/sync.rb. It fetches sponsor records, walks GitHub pagination, matches sponsors against linked Discourse accounts, then updates group membership in both directions: add the active people, remove the stale ones.

# Pseudocode for the sync loop
sponsors = fetch_sponsors
linked_accounts = UserAssociatedAccount.where(provider_name: "github")

active_users = match_sponsors_to_users(sponsors, linked_accounts)
add_to_sponsor_group(active_users)
remove_from_sponsor_group(users_no_longer_active)
log_sync_history(added: active_users, removed: stale_users)

That last line matters. Many integrations only know how to add. This one knows how to subtract, and that is what turns a reward script into a control plane.

A single sponsor record moves through a mechanical pipeline. It enters as a GitHub GraphQL query card, passes a linked-account gate, then splits into a forum badge stamp and a single-use Discord key while stale records drop into a revoked bin. The image explains how active sponsors are granted access and inactive ones are removed.
The pipeline is intentionally asymmetric. Active records move forward, stale records are caught and revoked.

Why GraphQL and rate-limit awareness matter

The GitHub side is built for efficiency, not convenience. Using the GraphQL shape for sponsorshipsAsMaintainer lets the plugin pull the sponsor login, type, and tier in one pass, which matters when the sync job runs repeatedly and needs to stay within rate limits.

The API wrapper is defensive by design. It watches the remaining quota, reacts to failure conditions, and plugs into Discourse’s problem-check system so a bad token becomes an operational issue instead of a silent one.

That is the right instinct for infrastructure. If the source of truth is wrong, the plugin should say so loudly.

Discord access is pull-based, not push-based

The Discord flow is a deliberate product choice. Users request an invite, the plugin verifies they are eligible, checks whether they are already in the guild, then issues a single-use link with an expiry and logs the result.

That is stronger than blasting invites automatically. It keeps the user action explicit, makes abuse harder, and leaves a trail when something needs to be explained later.

What this plugin does better than simpler sponsor tools

ProjectPlatformSponsor syncRevocationDiscord accessAdmin audit logsBest for
roots/discourse-github-sponsorsDiscourseYesYesYesYesSponsor-led community stacks
FriendsOfFlarum/github-sponsorsFlarumYesYesNoLimitedFlarum forums with sponsor groups
PHP GitHub SponsorsLibraryPartialNoNoNoCustom builds and framework work

The comparison is not about winners and losers. It is about scope. This plugin bundles the full entitlement workflow for a Discourse-first community, while the alternatives are either narrower or more like building blocks.

The admin surface is the real product

The dashboard is where trust gets maintained. Sync history, invite logs, manual runs, and problem checks give an administrator enough context to answer the only question that matters when a sponsor complains: why did access change?

That is the difference between an integration and an operational system. One tries to make a task disappear. The other makes the task explainable.

What this says about open-source monetization

Open-source funding is maturing. Communities no longer just want to thank sponsors. They want a system that ties money, identity, and access together without losing control of any of them.

That is what makes this repo interesting. It does not automate applause. It automates membership, and it keeps checking that membership against reality.