Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationPackagesNative invite acceptance

Native invite acceptance

This package lifts the native hosted login restriction that an organization-scoped callback requires a pre-existing kept membership. The GET /accounts/auth/workos begin/callback pair now covers the invited user’s first login: after WorkOS accepts the invitation and returns an organization-scoped authentication, API2 provisions the local membership and employee itself instead of failing with a generic auth error. Invitation creation, revocation, WorkOS invitation emails, the invitation.accepted / organization_membership.created webhooks, and the direct invite-email callback remain owned by Rails.

Contract

  • Membership resolution inside the hosted-login transaction mirrors Rails Authentication#organization_from_workos_response → Membership.find_or_create_from_workos exactly:
    • A kept membership is reused. A pending one is healed like #sync_status_from_workos!: read the user’s WorkOS organization membership (limit 1, three-second timeout, no retries), apply a valid status, fill a blank workos_membership_id, and never overwrite a present one. Role and scopes are preserved; no second employee is created.
    • A discarded membership means no access. The login still succeeds — tenantless session, no organization_id, no default_organization_id write — and the membership is never revived. (The hosted-login stage rejected this case; Rails never did.)
    • With no local membership at all, read the WorkOS organization membership for the authenticated user and provision: resolve the local role by persisted WorkOS slug first and key second, preferring this organization’s role over a global one; create the membership with the WorkOS status (a valid status, else pending), scope_type = 'organization', full_workspace_access = true, a Rails-format nanoid public_id, and the WorkOS membership ID; then run the #link_employee! equivalent — link the organization employee matching the account email (healing Anonymous/Unnamed Employee placeholder names from the account’s full name) or create one with evaluable = (role key == 'agent') and actor_type = 'human'.
  • All writes stay inside the existing hosted-login transaction under its advisory lock, so a WorkOS failure or identity conflict rolls back account, membership, employee and session changes together. A membership insert that loses the race to the Rails organization_membership.created webhook reuses the webhook’s row via the (account_id, organization_id) unique index.
  • Fail closed with the same generic auth error when WorkOS reports no membership for the user, when the WorkOS membership belongs to a different user or organization, or when the WorkOS role slug does not resolve to a local role. Logs keep the existing event names and carry no email, tokens or session data.
  • Only public.* legacy rows are written. The api2.users / api2.memberships bridge stays lazily reconciled by session validation (upsertSessionRecords), the same pattern hosted login and organization switch use.

Focused pglite tests cover first-login provisioning (role, status, employee, default organization, Rails-readable session), org-role preference and agent evaluability, existing-employee linking with placeholder-name healing, pending healing with and without a stored workos_membership_id, no WorkOS call for active memberships, tenantless discarded logins, fail-closed rollback for missing WorkOS memberships and unknown roles, and webhook-race reuse. Unit tests pin the WorkOS directory mapping and Rails’ role-slug extraction shapes.

Explicit Rails dependencies (open gates)

  • Direct invite-email callbacks. The link in the WorkOS invitation email authenticates on WorkOS’s hosted screens and redirects with a code but no API2-issued state or PKCE verifier, so the native callback cannot exchange it. That redirect must keep targeting the Rails callback (which accepts a nil state) until the invite email’s redirect flows through the native begin endpoint. First-party UI callers accept invites natively by starting at GET /accounts/auth/workos (optionally with organization_id / login_hint).
  • Invitation records. The invitations row’s pending → accepted transition, audit events, revocation and WorkOS invitation API calls remain on Rails (invitation.accepted webhook, InvitationsController).
  • Scoped invitations. Group/partner/team invitation scopes are applied only by the Rails organization_membership.created webhook, exactly as in Rails today: the login path (here and in Rails) provisions organization-scoped memberships and relies on the webhook when it lands first.
  • WorkOS-only roles. A role that exists only in WorkOS (created in its dashboard, never synced locally) fails the native login closed instead of being imported mid-login. Rails webhooks and the API2 WorkOS membership reconciliation sync own role import; a retry converges once the role exists locally.

Rollout

No schema change, no new endpoint, no flag: the behavior ships inside the already-deployed native hosted-login callback and uses the same WorkOS secret key the reconciliation sync reads. Before relying on it, exercise in each region: an actual invited-user first login landing with an active membership and linked employee, a pending-membership login healing to active, and a revoked-membership (discarded) login landing tenantless. Watch the existing native_hosted_login_completed / native_hosted_login_failed events for a failure-rate change after deploy.

Rollback is a code revert of this package; the hosted-login contract returns to requiring a pre-existing kept membership and Rails webhooks continue to provision memberships out of band. Provisioned rows are ordinary Rails-shaped memberships/employees and need no cleanup on rollback.

Last updated on