User-Tenant Onboarding and Membership
Overview
Section titled “Overview”The User-Tenant design covers self-serve multi-tenant onboarding and membership management. Users can self-sign-up, self-subscribe, and self-manage their tenant memberships.
The Accounts service stores users, tenants, and the affiliations that allow users to act for tenants. It also manages tenant invitations. These resources are bitemporal entities, so reads can address an effective and recorded point in time and history is retained.
Identity authentication is provided to Accounts through the application context. Accounts resolves that identity to a
UserAccount and, for tenant-scoped requests, to an active AgentFor. Cognito provisioning, JWT issuance, and the
transport mechanism that selects a tenant are outside this service’s implementation.
Key Entities (ERD)
Section titled “Key Entities (ERD)”Identity contains the primary email plus optional name, salutation, job title, phone, postal address, and named maps
of additional emails, phones, addresses, and sites.
CompanyInformation contains a company name plus optional legal name, country, tax ID, registration ID, NAICS code, and
postal address. UserAccount.companyInformation is accepted during creation as onboarding input but is not persisted on
the user account. When supplied, it is used to create the paid tenant described below.
Account Creation and Onboarding
Section titled “Account Creation and Onboarding”Only an authenticated service actor can create a user account through the regular service operation. A normal user, including a SaaS administrator acting as a user, cannot create one directly.
The creation orchestration has these effects:
- Accounts persists the user and forces its subscription to
ACTIVE, withacceptedTCset to the operation’s effective time. - If
companyInformationis present, Accounts creates aPaidtenant using that company information and the tenant ID fromactiveAgency. It creates anACTIVE/ADMINaffiliation. - For a paid tenant, configured customer-success users are added as
ACTIVE/ADMINmembers on a best-effort basis. Failure to find or add one of those users does not fail onboarding. - Accounts always creates a
Personaltenant with anACTIVE/ADMINaffiliation for the new user.
The personal tenant is named Personal tenant for <primary email>. Its company name is the same email address. Both
tenant types hold a subscription reference back to the creating user-account revision.
Tenant creation through the public tenant service is denied to user actors and is reserved for service/system flows.
Service Accounts
Section titled “Service Accounts”Opaque tokens are identified by a SHA-256 digest. A configured digest maps to a service email and SaaS-admin flag.
During actor resolution, Accounts finds or creates the corresponding technical UserAccount and synchronizes its
sha, isService, and isSaasAdmin fields. A service account must have a non-empty sha, and a non-service account
must not have one. Stale technical users can be pruned when their email is no longer configured.
Service accounts are created without tenant or affiliation creation.
Authentication and Tenant Scope
Section titled “Authentication and Tenant Scope”Secure endpoints are wrapped by an actor interceptor. It resolves one of these actors from the application context:
ServiceActorfor opaque-token authentication.GlobalActorfor an authenticated OIDC subject without tenant scope.TenantActorfor an authenticated OIDC subject with tenant scope.UnauthenticatedActorwhen no authenticated identity is present.
The actor is enriched from UserAccount. A tenant actor is then enriched from the matching AgentFor identified by
OIDC subject and tenant ID. The affiliation must exist and be ACTIVE; its role becomes the actor’s tenant role. SaaS
administrators may resolve tenant scope without an affiliation. Consequently, tenant isolation is enforced from the
resolved application scope and current affiliation rather than from an unchecked client-provided tenant value.
The Accounts code does not define an X-TENANT-ID header, so callers should not rely on that header based on this
service alone. Mapping an incoming request to ServiceScope.Tenant is an upstream runtime responsibility.
Account Access and Updates
Section titled “Account Access and Updates”- Tenant-scoped users can read accounts belonging to users affiliated with their current tenant. SaaS administrators can read all accounts. Non-admin global users and non-admin services cannot read accounts through these operations.
- A user can update their own non-email identity fields and settings. Their OIDC subject and primary email are immutable.
- A SaaS administrator can additionally change their own SaaS-admin flag and can promote or demote other SaaS administrators. The last SaaS administrator cannot be demoted.
- Regular update requests cannot change subscription, HubSpot ID, service credentials, service status, active agency, or onboarding company information.
- Only a SaaS administrator can delete a user account. Deletion first deletes all of the user’s affiliations.
Tenant Access and Updates
Section titled “Tenant Access and Updates”- Services can read tenants. SaaS administrators can read all tenants. A non-admin tenant actor can read and list only tenants in which their subject has an affiliation.
- Services may create tenants. User actors cannot create tenants directly.
- A tenant
ADMINmay update tenant name, company, and settings. A service may update only the plan. A SaaS administrator may update all four fields. - Only a SaaS administrator can delete a tenant. Deletion first deletes all affiliations for that tenant.
There is currently no special deletion guard for Personal tenants; the earlier statement that personal tenants cannot
be deleted is not enforced by this implementation.
Membership (AgentFor) Rules
Section titled “Membership (AgentFor) Rules”An affiliation uniquely joins one user account to one tenant. New affiliations must have status ACTIVE. Only role,
status, and settings are mutable; the user and tenant references are immutable.
- Roles are
USERandADMIN; statuses areACTIVEandSUSPENDED. - A user can read their own affiliations. A tenant-scoped user can also read affiliations in the current tenant. SaaS administrators and services can read all affiliations.
- Direct affiliation creation requires a SaaS administrator. System creation is also used by onboarding and invitation acceptance, subject to the target affiliation’s write-access rules.
- A tenant administrator or SaaS administrator can suspend and reactivate members and promote users. Administrators cannot change another member’s personal affiliation settings.
- A user may demote their own administrator affiliation, and a tenant administrator may demote an administrator, only if another active administrator remains.
- A user can delete their own affiliation; a tenant administrator can delete an affiliation in their tenant; a SaaS administrator can delete any affiliation. Removing or suspending the last active tenant administrator is rejected.
Deleting an affiliation is the implemented eviction mechanism. Future tenant-scoped requests fail actor resolution because the membership is missing; suspended affiliations are rejected as unauthorized.
Sign-Up Flow
Section titled “Sign-Up Flow”- User registers in Cognito (email + password).
- A PostConfirmation Lambda creates
UserAccount, personalTenant, andAgentForlinking the user to their personal tenant, then sets thecustom:tenantin Cognito. - The personal tenant provides freemium access and cannot be deleted.
- The frontend receives a JWT containing
oidc:subandcustom:tenant.
HubSpot Sign-Up Flow
Section titled “HubSpot Sign-Up Flow”- User pays in HubSpot.
- HubSpot triggers a webhook.
- Webhook creates the user in Cognito via
AdminCreateUser, then calls Accounts to createUserAccount, personalTenant,AgentFor. - Accounts also creates a paid tenant and a second
AgentFor. - BFF updates the Cognito user record with
custom:tenant = paid.tenant.id.
Sign-In Flow
Section titled “Sign-In Flow”- User authenticates in Cognito and receives a JWT.
- Frontend queries the Accounts service using
oidc:subto get user account details and list ofAgentForrecords. - The UI shows the current tenant from the JWT token and a mechanism to switch tenants. The choice is persisted for future sessions.
- All subsequent requests include the JWT header.
Invitation Flow
Section titled “Invitation Flow”- Any resolved tenant actor can create an invitation for an email address that is not already affiliated with the current tenant.
- Accounts supplies the invitation ID, inviter and tenant references,
PENDINGstatus, invitation time, configured expiration time, and an acceptance URL. Only non-reserved message parameters from the request are retained. - Accounts renders and sends the invitation email. A delivery failure changes the status to
PENDING_FAILEDand stores the provider outcome inmessageStatus. - The invitee can read an invitation addressed to their primary account email. Tenant-scoped users can read invitations for their current tenant, and SaaS administrators can read all invitations.
- The invitee may transition a pending invitation to
ACCEPTEDorREJECTED. Acceptance creates a uniqueACTIVE/USERaffiliation for the invitee and tenant. - A non-invitee with access may resend (
PENDING), cancel, or archive according to the current status. Resending resets the invitation and expiration timestamps and sends another email, subject to the configured no-op interval.
Statuses are PENDING, PENDING_FAILED, ACCEPTED, CANCELLED, EXPIRED, REJECTED, and ARCHIVED. Expiration is
computed when records are returned; EXPIRED and PENDING_FAILED cannot be set directly by API clients. At most one
non-archived invitation may exist for an invitee and tenant.
Evict Flow
Section titled “Evict Flow”The tenant admin selects a user, confirms eviction, and the system deletes the AgentFor. Subsequent requests from that
user with the same tenant will receive a 403.
Create Paid Tenant Flow
Section titled “Create Paid Tenant Flow”A user creates a paid tenant from their personal tenant. They become the tenant admin of the new tenant. A new
AgentFor is created linking the user to the new tenant.
Request Authentication
Section titled “Request Authentication”All user requests carry:
- JWT (from Cognito) identifying the user with
subandcustom:tenantclaims.
Account always validates the tenant id against the authenticated user’s memberships on every request to prevent horizontal privilege escalation.
Implementation Boundaries
Section titled “Implementation Boundaries”This document describes behavior present in the Accounts service. Frontend tenant selection, persistence of a user’s UI choice, Cognito lifecycle hooks, JWT generation, HubSpot webhooks, and BFF orchestration are not implemented here and must be documented by the components that own them.
Copyright: © Arda Systems 2025-2026, All rights reserved