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. Anowneris 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
usersrow; Phase D: globalidentitiesrow). - 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).
| Aspect | Tenant Member | End User |
|---|---|---|
| Created by | Invite, SCIM, signup-as-Owner | OAuth consent flow |
| Lifecycle row | tenant_memberships | tenant_end_user_states |
| Bulk-manageable | No (individual) | Yes (paginated, filterable) |
| Admin surface | Settings → Team | End Users (default workspace) |
| Suspension scope | Loses all admin access | Loses access only to this tenant |
| Identity ownership | The tenant tracks them | The user owns it (Phase D) |
| Default RBAC | Admin-tier roles via bindings | Plan-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.goCheckPrincipalActive. - 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_tierand reconcilesrole_bindingsaccordingly.
See also: