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 bycodex.set_member_role(domain owners only, never your own role, audited asmember.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.updatedis deliberately not handled: when the custom-roles add-on is removed Clerk rewrites every membership toorg: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_roleon 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, otherwiseviewer. Creation never overwrites an existing member row. - Org creation is not webhook-driven.
codex.bootstrap_orgruns during sign-up (needs the session’s active Clerk org) and creates the org row, the first domain_owner member, anorg.createdaudit row and a “GitHub Actions” API key. Webhook events for Clerk orgs with nocodex.orgsrow 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_invitestable 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
Sign-in fails with "No suitable key or wrong key type"
Sign-in fails with "No suitable key or wrong key type"
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.User signed in but sees no data / wrong org
User signed in but sees no data / wrong org
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).Role changed in Clerk but the app disagrees
Role changed in Clerk but the app disagrees
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.
Paid but features missing (entitlements empty)
Paid but features missing (entitlements empty)
Check
org_subscriptions for the org: plan_slug, status, features. The
webhook reads feature slugs off the subscription payload (with a Backend API
fallback). An empty features on an active paid plan means the plan in the Clerk
dashboard has no features attached — fix it there, then re-save the subscription
or replay the webhook.

