authgear / authgear/authgear-server
Organizations: a first-class B2B organization model
- Dominant language
- Go
- Stars
- 2k
- Forks
- 125
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 32
Description
## Problem
Authgear has no way to model "this user belongs to customer company X".
There are two scoping concepts today, and neither fits:
- A **project** (`app_id`) separates one Authgear customer from another. End users never see it.
- **Roles and groups** are flat within a project. A role says what a user may do, not who they act on behalf of.
So a project serving business customers has to put the company into the role key: `acme:admin`, `globex:admin`. This works, but:
- The number of roles grows with (companies × roles).
- The role key becomes a namespace the server does not understand and cannot validate.
- There is nowhere to store a company's name or metadata, and no way to list its members.
- Removing a user from a company means finding every role key that mentions it.
## Appetite
Two phases. Phase 1 is the entity, the Admin API, the Portal screens, and the token claims. Phase 2 is organization-scoped access tokens, which can come later.
## Solution
An `Organization` entity that users belong to, with roles assigned per membership.
Full spec, written in the `docs/specs/` style and following `roles-groups.md`: **[link](https://gist.github.com/Shivam8584/886f8542574830075fbe7fc6772727ce)**
The model:
- Three new tables: `_auth_organization`, `_auth_user_organization`, `_auth_user_organization_role`.
- One column added to `_auth_role`: a nullable `organization_id`. `NULL` means a project role, as today. Non-`NULL` means a role owned by one organization.
- Membership has a status (`active` / `suspended`), so a member can be suspended without losing their role assignments.
- Two new token claims: `.../user/organizations` and `.../user/organization_roles`.
- Additions to the Admin API, the resolver headers, the user listing filter, and `UserMutations`.
Everything follows the existing roles/groups conventions: `app_id` scoping through `SQLBuilderApp`, the same index patterns, the same mutation naming.
### Three decisions worth your input
**1. Can an organization define its own roles?**
I say yes, but only as an exception.
My first draft said no — one role vocabulary for the whole project, organizations only assign from it. That is simpler, but it means an organization's own admin can never create a role. Every such request has to go to the project operator. That rules out delegated administration.
So: project roles are shared by default, and an organization can add roles of its own when it needs to. What I did **not** do is give every organization a full copy of the vocabulary, because that brings back the (companies × roles) growth this feature is meant to remove.
This is the part of the design I am least certain about, and it is where other products disagree:
| Product | Approach |
|---|---|
| WorkOS | Environment roles shared by all orgs, plus org-level custom roles |
| Auth0 | Org-scoped roles exist, but are Early Access |
| Keycloak | Reuses realm roles; 26.6 added per-org *groups*, not roles |
| Zitadel | Roles belong to projects, shared across orgs via grants |
**2. The new claims are scope-gated. The roles claim is not.**
`ClaimAuthgearRoles` is set unconditionally in `PopulateUserClaimsInIDToken`. That is fine for roles, because a project has a small fixed set of them.
`organization_roles` is different. It is an object with one key per organization the user belongs to. A user in 300 organizations produces a large claim, in every token, for every client — including clients that do not care about organizations.
So these two claims sit behind a new `organizations` scope, following how standard attributes are handled rather than how roles are.
**3. No `ON DELETE CASCADE`.**
`ON DELETE` does not appear anywhere in the migrations today. `Store.DeleteRole` cleans up `_auth_group_role` and `_auth_user_role` with explicit statements instead.
The spec follows that, and writes out the deletion order for each case. A single cascade hidden in one table is the kind of thing the next reader would not expect.
## Rabbit holes
**Organization-scoped tokens do not need new token machinery.** `EncodeClientAccessToken` already sets `aud` from the resource URI, so RFC 8707 gives us the custom-audience path. Phase 2 is the `organization_id` parameter, a membership check, and deriving the audience.
**Filtering scopes by role is not possible today.** A natural part of phase 2 would be narrowing a token's scopes to what the user's roles in that organization allow. But nothing connects roles to scopes: `_auth_client_resource_scope` grants scopes per *client*, roles are granted per *user*. Building that link is a separate design question — it decides whether roles are labels the application reads, or permissions the server enforces. The spec flags this as a prerequisite instead of assuming it.
**Delegated administration needs more than this spec.** The Admin API is authenticated project-wide, so organization admins cannot manage their own organization yet. As written, this is a complete organization *model* with project-level administration. The schema is built so that org-scoped admin credentials can be added later without changing it.
**The claim path touches several files at once.** Roles show the pattern: a computed per-user value has to go through the userinfo service, the ID token issuer, and the resolver's session info together. Miss one and the value is right in the database but missing from tokens.
**Removing a member does not revoke their tokens.** Same as roles today, but it matters more here, since removing a member is often how a company offboards someone. Called out in the spec.
## No-goes
Left out on purpose. Each can be added later without changing the schema:
- **Invitations.** Needs its own delivery and expiry design. The membership status column is there from the start so this stays additive.
- **Per-organization identity providers** with email-domain routing. This is the biggest gap, and most comparable products have it. Left out because domain verification is a subsystem of its own.
- **Organization-level settings** such as branding or per-org MFA.
- **Machine-to-machine membership** (applications as members).
- **Organization switching in the Auth UI.** Needs phase 2, and a "current organization" on the session, which does not exist.
- **Role-derived scopes.** See rabbit holes.
- **Nested organizations.**
---
Happy to implement this if it is a direction the team wants. I would want agreement on the role model first, since that decides the schema.
Contributor guide
Research direction
Start by reading the linked organization spec and the existing roles-groups.md conventions; the issue says the role model needs team agreement before implementation. For token claims, the named entry point is PopulateUserClaimsInIDToken, and EncodeClientAccessToken is mentioned for the later phase. Done means the agreed Phase 1 organization model, Admin API, Portal screens, and claims are implemented and covered across the relevant flows.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, authentication, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100