Files
ops-docs/maintenance/provisioning-boundary.md
T

4.5 KiB

Member provisioning boundary — who auto-provisions from Open Collective

Which Open Collective contributors become portal members automatically, and which are deliberately excluded. The portal defines the member set in fetch_members() (member-portal repo, app/main.py), acts on it in the webhook and the nightly reconcile_memberships() sweep, and exposes two admin overrides: POST /admin/org-link and POST /admin/provision. A daily watchdog (below) is the net for anything that slips past both.

Auto-provisioned

Individual OC accounts holding BACKER/ADMIN on the Membership tier (or no tier — pre-tier contributors are grandfathered) whose email is visible to the admin token. The webhook provisions on first payment; the nightly sweep (03:00 scheduler task) provisions anything missed within 24h and deactivates lapsed members. fetch_members() matches the tier by NAME ("membership", case-insensitive): renaming the tier on Open Collective silently empties the member set — the sweep's fail-safe skips reconciliation rather than deactivating everyone, but provisioning also stops.

Deliberately excluded

  1. Org-backed memberships. An ORGANIZATION account never exposes an email (the ... on Individual { email } fragment matches nothing, and OC returns org emails as null even to collective admins), so reconcile cannot map one and does not try. Org-backed memberships become members only when LINKED: POST /admin/org-link {"org_slug", "email", "name"} runs the full provisioning pipeline and stores members.org_slug; the sweep keeps linked accounts active while the org holds BACKER/ADMIN on the Membership tier and deactivates them when the org's dues lapse (fail-safe: if the org fetch itself errors, linked accounts stay active). An unlinked org's first payment emails info@ with the exact linking command.

  2. Non-membership tiers (Credit pack). A one-time purchase grants BACKER role but is NOT a membership — fetch_members() filters it out. This is why a contributor can pay, appear in the public backer list, and still have no portal member row: it is a credit-pack purchase, not a lapsed provisioning. The money is not lost: the webhook's held-credit-pack flow records a hold, notifies the buyer, and the operator grants, refunds or transfers it via /admin/credit-packs-held; a hold granted to a named member auto-applies if that person later becomes a member.

  3. Manual members. The MANUAL_MEMBERS env allowlist (founder/free tier, the co-op's own accounts) holds members with no OC membership BY DESIGN: never auto-provisioned from OC, and exempt from lapsed-membership deactivation. Check for orphaned LiteLLM member:* teams after any MANUAL_MEMBERS change.

  4. Hidden-email / anonymous contributors. An individual whose email is null even in the ADMIN view cannot be mapped by reconcile. They are not silently dropped — the daily watchdog reports them with their public dues, and provisioning is manual: POST /admin/provision {"email", "name"} on the operator's instruction. Incognito accounts publish an incognito-xxxxxxxx slug, but the ADMIN member query usually still reveals their email — always check the admin view before treating one as unmappable (that is what resolved the mmoya case).

The watchdog: oc_unmapped_check.py

Daily no-agent cron on the agent host (07:20, job d0fd1126cc80), running ~/.hermes/scripts/oc_unmapped_check.py — the same silent-until-findings pattern as the spend-consistency guard. It reads the collective's backer list through the portal env's admin token and the portal rows read-only via cloudron exec, maps them by email, slug or org_slug, and prints ONLY member-ish backers (the exact set fetch_members() treats as members) with no portal row at all: slug, display name, whether the admin view sees an email, dues (amount/frequency/status from the public order list), days as a backer, and the matching action (org-link vs /admin/provision vs investigate-the-sweep). Backer nodes with no account object (deleted/incognito-hidden accounts) are attributed via same-day public orders and stay silent when every same-day payer is already mapped. Excluded from findings: credit-pack-only backers (the held-credits flow owns them) and backers matching an inactive portal row (the sweep re-provisions/reactivates those within 24h). Empty output = clean; any fetch failure prints FAILED — a check that cannot look never reads as clean.