Skip to content

User-Tenant Onboarding and Membership

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.

PlantUML diagram

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.

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:

  1. Accounts persists the user and forces its subscription to ACTIVE, with acceptedTC set to the operation’s effective time.
  2. If companyInformation is present, Accounts creates a Paid tenant using that company information and the tenant ID from activeAgency. It creates an ACTIVE/ADMIN affiliation.
  3. For a paid tenant, configured customer-success users are added as ACTIVE/ADMIN members on a best-effort basis. Failure to find or add one of those users does not fail onboarding.
  4. Accounts always creates a Personal tenant with an ACTIVE/ADMIN affiliation 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.

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.

Secure endpoints are wrapped by an actor interceptor. It resolves one of these actors from the application context:

  • ServiceActor for opaque-token authentication.
  • GlobalActor for an authenticated OIDC subject without tenant scope.
  • TenantActor for an authenticated OIDC subject with tenant scope.
  • UnauthenticatedActor when 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.

  • 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.
  • 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 ADMIN may 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.

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 USER and ADMIN; statuses are ACTIVE and SUSPENDED.
  • 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.

  1. User registers in Cognito (email + password).
  2. A PostConfirmation Lambda creates UserAccount, personal Tenant, and AgentFor linking the user to their personal tenant, then sets the custom:tenant in Cognito.
  3. The personal tenant provides freemium access and cannot be deleted.
  4. The frontend receives a JWT containing oidc:sub and custom:tenant.
  1. User pays in HubSpot.
  2. HubSpot triggers a webhook.
  3. Webhook creates the user in Cognito via AdminCreateUser, then calls Accounts to create UserAccount, personal Tenant, AgentFor.
  4. Accounts also creates a paid tenant and a second AgentFor.
  5. BFF updates the Cognito user record with custom:tenant = paid.tenant.id.
  1. User authenticates in Cognito and receives a JWT.
  2. Frontend queries the Accounts service using oidc:sub to get user account details and list of AgentFor records.
  3. The UI shows the current tenant from the JWT token and a mechanism to switch tenants. The choice is persisted for future sessions.
  4. All subsequent requests include the JWT header.
  1. Any resolved tenant actor can create an invitation for an email address that is not already affiliated with the current tenant.
  2. Accounts supplies the invitation ID, inviter and tenant references, PENDING status, invitation time, configured expiration time, and an acceptance URL. Only non-reserved message parameters from the request are retained.
  3. Accounts renders and sends the invitation email. A delivery failure changes the status to PENDING_FAILED and stores the provider outcome in messageStatus.
  4. 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.
  5. The invitee may transition a pending invitation to ACCEPTED or REJECTED. Acceptance creates a unique ACTIVE/USER affiliation for the invitee and tenant.
  6. 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.

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.

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.

All user requests carry:

  • JWT (from Cognito) identifying the user with sub and custom:tenant claims.

Account always validates the tenant id against the authenticated user’s memberships on every request to prevent horizontal privilege escalation.

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.