Self-Serve Multi-Tenant Onboarding & Membership
We need a clear, end-to-end design for how users and tenants are created, associated, and managed in a self-serve, multi-tenant product.
Context & Assumptions¶
- Users can self sign-up.
- Users can self-subscribe to paid plans on behalf of the tenant they represent.
- Tenants (through their administrators) and Users can self-manage their membership.
- Every new user may begin with a free “print card” personal tenant if this makes the implementation simpler.
What the design must define¶
-
Tenant creation timing
- Precisely when a tenant is instantiated in the lifecycle (e.g., at account creation, after email confirmation, or upon first product action).
-
Initial user–tenant association
- When and how the first tenant is linked to a new user.
- If association happens at sign-up, specify the exact flow and artifacts created.
- If association does not happen at sign-up, specify the post-sign-up UX (what the user sees and how they proceed to create or join a tenant).
-
Multi-tenant membership
- Support for users belonging to multiple tenants.
- The mechanism and UX for selecting the “active” tenant within a session.
- Source of truth for the list of tenants per user (data model, storage, and retrieval).
-
Adding users to a tenant
- By invite: roles, tokens/links, validation, and acceptance flow.
- By request: discovery, approval workflow, notifications, and resulting membership state.
Main Design Points¶
- User sign-up
- Create personal tenant (aka “print card”)
- Note: At the moment, the personal tenant provides only one page with [upgrade], [Call us] buttons - User can edit their tenant
- Create Paid Tenant (like GH Org) - Vanilla JWT token
- Cognito’soidc:subassociates JWT toUserAccount
- FE queriesAccountfor allagentFor(oidc:sub)
- HeaderX-TENANT-ID+ JWT associate requests to a user in a tenant
Pending Decisions¶
- How does the system track the preferred
AgentForfor a user at login?- default agent: Explicit choice by the user, persisted for future sessions.
- current agent: Implicit choice: most recently used agent.
- How are paid plan confirmed with business?
- How are paid plans connected to financial system?
- How granular is the API provided to the FE?
- Just
Accountservice? - Or
UserAccount,AgentFor,Tenants?
- Just
Initial Simplifications¶
- No federation with external identity providers (IdP).
- Any user can create a paid tenant.
- No user roles beyond regular user.
- Initial implementation supports only regular users; support for user roles (via RBAC/ABAC) is planned.
- Time to market is the priority and multi-tenant affiliations or “self-tenant” at sign-up can be sacrificed
if it simplifies and accelerates the implementation. - The mechanism to restrict access to features or enforce limits based on the plan is out of scope for this release and a much bigger topic.
- The commercial tracking of a tenant and its subscription will be done by HubSpot and Stripe. The integration with those systems is out of scope for this phase.
Sequence Diagrams¶
Sign-Up and Sign-In¶
The sign-up and sign-in flows converge after authentication; this discussion focuses on the sign-up flows,
as the sign-in is a very simplified version of it.
Sign-Up¶
On sign-up, the user is created in Cognito after email confirmation. (Currently no email confirmation is required.).
Cognito tracks the minimum user information (name, email, password hash, and oidc:sub).
The creation of the user in Cognito triggers a PostConfirmation Lambda, that creates a UserAccount,
a personal Tenant, and an AgentFor linking the user to the tenant.
The UserAccount is the core entity representing a user in the system, and is linked to Cognito via the oidc:sub.
It stores the user profile (name, email, photo, …) and preferences that apply across all tenants
a user might belong to.
The Tenant is the entity associated with a name and subscription plan that represents a legal entity/organization
that has a contract with Arda. A tenant’s configuration
contains settings that specify the application behavior for that tenant for all users.
The AgentFor links a UserAccount to a Tenant, and contains the RBAC role, ABAC policies,
and preferences that apply to the user in the context of the Tenant linked by that agent. Amongst other things,
it records if this agent is the default agent for the user. (Maybe also the most recently “active” one?)
Every user is automatically associated with a personal tenant at sign-up.
That personal tenant implements the free print a card plan, and cannot be deleted.
That personal tenant offers the user the ability to create paid tenants later.
At the end of the sign-up flow, Account contains three new connected entities (UserAccount, Tenant, AgentFor),
with the AgentFor for the personal tenant selected as the default agent.
The front end obtains a JWT containing the oidc:sub from the OAuth2 Client Application in Cognito.
Sign-In¶
On sign-in, the user is authenticated in Cognito, and receives a JWT containing the oidc:sub.
Post Sign-In/Sign-Up¶
Once the front end has the JWT, it queries the Account service for the user account details.
Account uses the oidc:sub to look up the UserAccount, and returns the account details.
Amongst others, the account details contain the list of AgentFor records associated with the
user, as well as a default tenant. Should there be no default agent, or multiple default agents,
the front end selects the agent with the most recent activity.
The UI shows the current tenant and provides a mechanism to switch to a different tenant
(and associated AgentFor). The choice is persisted for future sessions.
The user is now logged in and ready to proceed.
All subsequent requests to the backend services include the JWT, identifying the user,
and the X-TENANT-ID, identifying the tenant for the selected agent, in the headers.
Security Note: The backend does not trust the client-supplied
X-TENANT-IDalone.
On every request, the backend re-validates that the provided tenant/agent is among the
authenticated user’s memberships before honoring the request. This prevents horizontal
privilege escalation and ensures users can only access tenants they are authorized for.The backend then adopts the RBAC/ABAC policies and preferences.
The backend rejects the request if no such
AgentForexists, or the request isn’t
authorized by the RBAC/ABAC.
Due to the nature of the process, this sequence diagram makes explicit the distinction between the
Single Page Application (SPA), which executes in the browser, and the Backend For Frontend (BFF),
hosted by AWS. They are subsumed under the Front End (FE) in other diagrams.
Invite¶
At the moment, this scenario supports “out of system” invitations. In the future, the system itself
will be able to send the Invitation notification.
The invite flow allows a user, the tenant admin, to invite others to an existing tenant.
The tenant admin generates an invitation URL and shares it with the invitee as a link in an email or message; the
link could also be shared as a QR code. The invitee can be an existing user or a new user.
The invitation is valid for a limited time (e.g., 24h) and includes the tenant id and the invitee’s email.
The invitation itself is stored in the database, the URL only contains the invitation UUID.
The person opens the invite URL, and if needed, signs up or signs in. They are then presented with a confirmation page.
If they accept, the system creates a new AgentFor connecting the UserAccount with the Tenant and marks the invitation
as accepted. Otherwise, the invitation is marked as declined.
Note
MP:
Need to think how the FE will handle internal failures (e.g., Tenant does not exist,
inviter permissions, …) Probably just go to a failure page with instructions
to contact the inviter or Arda support?
Evict¶
The evict-flow allows a user, the tenant admin, to terminate an existing agent’s membership in a tenant.
The tenant admin opens the user administration page, selects the user to evict, and confirms the action.
The system removes the AgentFor connecting the UserAccount with the Tenant.
Any future requests from that user with the same JWT and X-TENANT-ID will be rejected with a 403,
and the response will not contain the X-TENANT-ID. The user can still sign in and access other tenants they belong to.
Create Paid Tenant¶
The create paid tenant flow allows a user to create a new paid tenant from their personal tenant.
A user can create a paid tenant from their personal tenant. They become the tenant admin of the new tenant.
ER¶
This section outlines all the entities and their relationships in the user-tenant system. It doesn’t specify the nature
of their attributes beyond the essentials, nor does it include audit fields (created_at, updated_at) for clarity.
Note
MP:
Need to check with the Accounts Information Structure but right now I like this one better.
Requests Authentication¶
Communication between the SPA and the BFF¶
The Single-Page Application (SPA) communicates with the front end (BFF) using REST over HTTPS.
It authenticates using an Authentication: Token JWT.
The JWT is obtained from Cognito during the Sign-In scenario.
Communication between the BFF and the backend service¶
The front end (BFF) communicates with the backend services using REST over HTTPS.
It authenticates using an Authentication: Token JWT.
The BFF performs a machine-to-machine OAuth2 authorization to obtain the JWT.
The communication is authorized by the API Gateway and by the backend services.
Communication between post-confirmation lambda and the backend service¶
The post-confirmation lambda communicates with the backend services using REST over HTTPS.
It authenticates using an Authentication: Token JWT.
The lambda performs a machine-to-machine OAuth2 authorization to obtain the JWT.
The communication is authorized by the API Gateway and by the backend services.
Communication between Cognito and the backend service¶
Cognito exposes the oauth2 and oidc REST endpoints
/oauth2/authorize/oauth2/token/oauth2/userinfo/oauth2/revoke/.well-known/jwks.json/.well-known/openid-configuration
The Single-Page Application (SPA), the front end (BFF), and the backend services
communicate with Cognito using REST over HTTPS.
They authenticate using an Authentication: Token JWT.
The JWT is obtained from Cognito during the Sign-In scenario.
Copyright: © Arda Systems 2025, All rights reserved