Skip to main content

Members vs End Users

Every AuthSec tenant has two kinds of users. They live in different tables, are managed on different admin pages, and play different roles in your product.

Tenant Members

Members are operators. They administer the tenant — register Applications, write authz policy, approve OAuth clients, review audit logs, suspend abusive end users.

  • Small, finite set (you don't have a million of them).
  • Lifecycle: invited → active → suspended → left.
  • Stored in tenant_memberships(tenant_id, user_id, status, membership_type, source, …).
  • Managed in Settings → Team in the admin UI.
  • Receive admin-tier permissions via role_bindings (e.g. Security Admin, MCP Admin).
  • Membership types: owner, admin, member, contractor, service_operator, readonly_auditor. These are lifecycle metadata, not RBAC roles. An owner is still bound to permissions via a role binding.

End Users

End users are consumers of your tenant's published Applications. Anyone who connects an AI client to your MCP server, signs into your web app, or grants a workload access to your APIs is an end user.

  • Potentially huge in number (10k, 100k, more).
  • They have a global identity (Phase A: per-tenant users row; Phase D: global identities row).
  • Per-tenant state lives in tenant_end_user_states(tenant_id, user_id, status, plan_tier, rate_limit_override, …). Created lazily on first consent.
  • They are not members of your tenant. They never appear in tenant_memberships.
  • Managed in End Users (the default workspace) in the admin UI.
  • A tenant can:
    • Suspend an end user (blocks every token they hold against your Applications) without affecting their identity globally.
    • Assign a plan tier (free, pro, custom). Plan tiers map to role bindings via your billing policy.
    • Override rate limits on a per-user basis.

Why this split matters

The most common mistake when modeling auth for a public product is treating end users like members: storing them in a users table, manually inviting them, giving every signup a role assignment. That works at small scale, then collapses.

A solo dev launching a public MCP server can have 100,000 end users with a single tenant and zero tenant_memberships rows except their own (owner).

AspectTenant MemberEnd User
Created byInvite, SCIM, signup-as-OwnerOAuth consent flow
Lifecycle rowtenant_membershipstenant_end_user_states
Bulk-manageableNo (individual)Yes (paginated, filterable)
Admin surfaceSettings → TeamEnd Users (default workspace)
Suspension scopeLoses all admin accessLoses access only to this tenant
Identity ownershipThe tenant tracks themThe user owns it (Phase D)
Default RBACAdmin-tier roles via bindingsPlan-tier roles via bindings

End-user identity is global; tenant relationship is local

A single human can be:

  • An owner of alex-tools (their own tenant)
  • A contractor member of acme-corp (a client tenant)
  • An end user of data-tools-cloud (a public provider they use personally)

That's one identity, two tenant memberships (owner + contractor), and one tenant_end_user_states row. Phase D unifies the underlying identity model so all three rows reference the same global identities row.

In code

  • Reading membership status is a precheck before any role binding is evaluated. See services/rbac_service.go CheckPrincipalActive.
  • End-user suspension updates tenant_end_user_states.status='suspended'. The next introspection of any token issued to that user against any of the tenant's Applications fails closed.
  • End-user plan tier changes are usually triggered by a billing webhook into AuthSec, which updates tenant_end_user_states.plan_tier and reconciles role_bindings accordingly.

See also: