Domain join & license access
How users with a corporate email domain discover and join an existing license, and how SSO can link them directly. Logic lives in the PHP api and surfaces in web-presentations.
Account owner (app definition)
The license owner is the user on the account with role = admin and createdByUserId IS NULL (User::findAccountOwner). This may differ from accounts.accountOwner and from your support contact.
How accounts become join-eligible
Accounts::getJoinEligibleAccounts() returns targets when all of the following hold:
- User domain is not marked generic in
domains. - Account has
allowsJoinRequests = 1. - Match via owner email domain (plan with REQUEST_LICENSE_ACCESS or legacy paid:
users > 1andaudienceSize > maxFreeAudienceSizefrom the checking user’s branding) oraccount_domainsrow. - Account owner has a non-expired session (owner-match path only).
isAutomaticallyJoinable does not affect discovery — only which email/path runs after the user is eligible.
UserLicenseFlow cases
Returned to web-presentations as userLicenseFlow from GET users/flow.
| Case | Meaning |
|---|---|
| NOT_ELIGIBLE | No matching accounts or generic email domain |
| MULTIPLE_ELIGIBLE | Two or more joinable accounts — user picks in web-presentations |
| LICENSE_ELIGIBLE | One account; plan has LICENSE_AUTO_APPROVAL |
| LICENSE_REQUEST_ACCESS_ELIGIBLE | One account; plan lacks auto-approval feature |
| LICENSE_PENDING | User has unused account_activation_tokens row |
Join flows
Signup → domain join funnel
New email/password users with a matching domain may receive a join email at signup. Redirect params: welcome, license-eligible, or multi-license.
api/common/components/signup/SignupComponent.phpSelf-join via verification email
When LICENSE_AUTO_APPROVAL + isAutomaticallyJoinable + seats available, user gets join-license link. Click adds them via addFreeUserToExistingAccount.
api/api/controllers/LicenseController.php → actionJoinLicenseAdmin approval path
When auto-join conditions fail or isAutomaticallyJoinable is off, user confirms request-license-access link; admin accepts in team UI.
api/common/models/Accounts.php → acceptJoinRequestSSO auto-join
identity_provider_identifiers.accountId in OAuth state → addExistingUser on callback. Bypasses domain eligibility checks.
api/common/modules/identityprovider/components/IdentityProviderCognito.phpDatabase flags & settings
| Setting | Default | Set by | Effect |
|---|---|---|---|
| accounts.allowsJoinRequests | 1 | Account admin (if plan allows) | Master switch for domain join discovery |
| accounts.isAutomaticallyJoinable | 0 | Support (st-admin) | Self-verify email vs admin-approval email |
| accounts.domainCheck | 0 | Account admin + plan | Restrict add-user emails (with TOGGLE_DOMAIN_CHECK feature) |
| account_domains.domain | — | Support (st-admin) | Extra join-eligible domains + domain-check exceptions |
| domains.isGeneric | 0 | Support (st-admin) | 1 = public provider — blocks join discovery |
| Plan: REQUEST_LICENSE_ACCESS | per plan | Plans & features | Required for join via plan; enables admin toggle |
| Plan: LICENSE_AUTO_APPROVAL | per plan | Plans & features | LICENSE_ELIGIBLE case; part of auto-verify path |
| Plan: TOGGLE_DOMAIN_CHECK | per plan | Plans & features | Makes domainCheck effective |
| identity_provider_identifiers.accountId | null | Support (st-admin) | SSO users auto-join this account |
Auto-join decision (email path)
LicenseController::attemptToRequestLicenseAccess sends a self-join verification email when all are true:
canAutoAcceptUsers() // plan feature LICENSE_AUTO_APPROVAL
isAutomaticallyJoinable // accounts column
!getReachedMaximumUsers() // seats available Otherwise the user gets a request-access link; the admin must accept via acceptJoinRequest.
SSO (separate fast path)
When Cognito OAuth state contains accountId (from identity_provider_identifiers), the authorize callback calls addExistingUser immediately — no domain eligibility, no verification email.
fullEmailDomain— matches user email domain to IdPaccountId— target account for auto-joinexclusions— comma-separated emails blocked from SSO URLlocation— label shown on login picker
Domain check (add-user restriction)
Separate from self-join. When domainCheck = 1 and plan has TOGGLE_DOMAIN_CHECK, admins can only add users whose email domain matches the owner or appears in account_domains (User::compareEmails).
web-presentations routes
/license-eligible → verification email sent overlay
/multi-license → pick from multiple accounts
/join-license → welcome after successful email join
/requested-access → admin request confirmed
/request-access-failedst-admin API (support tooling)
GET /api/account-join/accounts/{id}
PUT /api/account-join/accounts/{id}
POST /api/account-join/accounts/{id}/domains
DELETE /api/account-join/accounts/{id}/domains/{domain}
GET /api/account-join/domains/{id}
PUT /api/account-join/domains/{id}
PUT /api/account-join/identity-provider-identifiers/{id}
GET /api/account-join/preview?email=user@company.com