Since 2026-08-12, Clerk owns auth, organizations and billing. Member roles came back under the app’s control on 2026-08-19 — Clerk’s custom org roles are a paid add-on and were only ever an editor, since every gate already read codex.members.role from Postgres. Supabase Auth is fully retired — nothing calls supabase.auth.*, and auth.uid() errors on Clerk tokens. Supabase is wired to Clerk as a third-party auth provider, which is what lets PostgREST validate Clerk-issued JWTs for RLS.

The sync pipeline

Facts that matter in support:
  • Role source of truth is codex.members.role — the column RLS, the RPCs and the worker have always read. It is written by codex.set_member_role (domain owners only, never your own role, audited as member.role_changed) from Settings → Members & roles. No JWT carries a role claim, so a change takes effect on the very next request.
  • Clerk no longer sets roles. organizationMembership.updated is deliberately not handled: when the custom-roles add-on is removed Clerk rewrites every membership to org:admin/org:member, and handling that event would have mass-downgraded real roles. Any role shown in Clerk’s own panel is cosmetic.
  • Starting role on join: public_metadata.codex_role on the Clerk membership if present (this is how a role can travel with an invitation without the add-on), otherwise the mapped Clerk role key for orgs still on custom roles, otherwise viewer. Creation never overwrites an existing member row.
  • Org creation is not webhook-driven. codex.bootstrap_org runs during sign-up (needs the session’s active Clerk org) and creates the org row, the first domain_owner member, an org.created audit row and a “GitHub Actions” API key. Webhook events for Clerk orgs with no codex.orgs row are deliberately ignored.
  • Invites are Clerk’s end to end (email, acceptance, role picked at invite time). The webhook lands the membership; there is no pending_invites table any more.

Role capabilities (application-level)

Reviewer sign-off is a hard gate before approval; the domain owner retains an approve override but cannot skip the reviewed state. Saving a new revision invalidates an existing review. Group membership, not role, is what authorises review and approval — the approver role name is a convenience, not the gate. Any non-viewer member of an RFC’s assigned approver group may approve it, including a plain author; that is the point of assigning groups per RFC. The two floors are: viewers may never review, approve or comment (viewer is read-only everywhere), and no RFC can be approved without a review sign-off, which needs a reviewer group to have been assigned in the first place — an RFC with no reviewer group is a dead end until the domain owner assigns one. Archived RFCs are frozen: transition_rfc, submit_review and save_revision all refuse them (migration 20260819100000_workflow_guards.sql). duplicate_rfc deliberately still works — copying an archived RFC into a fresh draft is the documented revive path.

Entitlements (plan gating)

codex.has_entitlement(org_id, feature) is the single server-side gate, reading webhook-synced org_subscriptions.features. Known feature slugs include github_scanning and llm_extraction (the team plan currently carries seven — the list comes live from Clerk Billing, never a hardcoded map). PR-diff checks (/v1/check) are deliberately not gated — they are the free plan’s core loop. Full-repo scans check the entitlement at execution time, so a downgrade cancels already-queued scans.

Known failure modes

The Supabase side of the Clerk integration is missing. Both halves are required: Supabase Dashboard → Auth → Third-Party Auth must list the Clerk domain, and Clerk’s Supabase integration must be enabled (it adds the role: authenticated claim). This exact pairing broke first sign-in during the migration.
RLS is working — the user has no members row for that org. Check codex.members for their user_… id; if missing, the membership webhook may not have landed (check Clerk webhook delivery logs, endpoint clerk-webhook).
The webhook syncs within seconds normally. Verify the Clerk webhook endpoint is healthy and that the role key is one of the mapped ones — an unmapped key is skipped by design on updates.
Settings → Members & roles, where the Clerk organization panel manages invitations and roles