Skip to content

Cross-Tenant SSO

Cross-tenant SSO allows a user who has registered at Company A to link their account and log in to Company B using the same credentials. This works across different tenants and across sub-companies within the same tenant. Identity is unified by email or phone number through a GlobalIdentity layer, while each portal's business data (tokens, vouchers, etc.) remains strictly isolated. A single blockchain wallet is shared across all linked portals.

Overview

ItemDetail
ScopeMember App only
Identity keyEmail or phone number
Session carrierHttpOnly cookie (vio_sso_token)
Token modelJWT access/refresh tokens remain portal-specific (one per tenant + sub-company combination)
WalletOne blockchain wallet (same address) shared across all linked portals
Data isolationEach portal (tenant + sub-company) has its own User record and business data
Portal keyA portal is identified by the composite key (tenantId, subCompanyId). The parent company uses subCompanyId = null.

Architecture

┌──────────────┐   cookie    ┌─────────────────┐   cookie    ┌──────────────┐
│  Company A   │ ──────────► │  SSO Session    │ ◄────────── │  Company B   │
│  Member App  │             │  (Global)       │             │  Member App  │
└──────┬───────┘             └────────┬────────┘             └──────┬───────┘
       │                              │                             │
       │ JWT (portal-a)               │ GlobalIdentity              │ JWT (portal-b)
       │                              │                             │
       ▼                              ▼                             ▼
  User (portal-a)            Email / Phone              User (portal-b)
       │                     Password (hashed)                │
       └────────────────► Shared Wallet Address ◄─────────────┘

A "portal" is a unique combination of tenant and sub-company. For example, Tenant A's parent company and Tenant A's Sub-Company X are two different portals within the same tenant. Users can SSO between them just like they SSO between different tenants.

Key Models

ModelPurpose
GlobalIdentityStores unified email/phone + hashed password. Links to User records across portals via linkedTenants[], where each entry contains tenantId, subCompanyId, and userId.
SSOSessionRepresents an active global login session. Token stored in an HttpOnly cookie.
User.globalIdentityIdReference from a portal-specific user to its global identity.
User.walletAddressShared blockchain wallet address. All linked users reference the same address.

Portal-Keyed Identity

Each portal is identified by its tenant and sub-company (if any). A single person can have separate member profiles at:

  • Different tenants (e.g. Company A and Company B)
  • The same tenant's parent company and its sub-company (e.g. Company A parent and Company A / Branch X)
  • Different sub-companies within the same tenant (e.g. Company A / Branch X and Company A / Branch Y)

User Flow

1. First Registration (Company A)

  1. User registers at Company A.
  2. Backend creates a GlobalIdentity (if new) and links the Company A User.
  3. An SSOSession is created; its token is set as an HttpOnly cookie.
  4. User receives tenant-specific JWT tokens for Company A.
  5. A custodial blockchain wallet is created; its address is stored on the User record.

2. Cross-Tenant & Cross-Sub-Company Scenarios

The following scenarios apply to both cross-tenant linking (different tenants) and cross-sub-company linking (same tenant, different sub-company). The flows are identical — the system detects the mismatch and prompts the user to link.

There are three scenarios for how a user can link to a new company or sub-company:

  1. User opens Company B's Member App and navigates to the Sign Up page.
  2. User enters an email or phone number that is already registered in Company A.
  3. Frontend calls POST /api/auth/sso/check-identifier to detect cross-tenant accounts.
  4. A popup appears: "You have already registered in Company A using this account. Would you like to link your account to Company B and use the same credentials to log in?"
  5. If the user clicks Yes, they are redirected to Company B's Login page.
  6. User enters the same email and password, then clicks Log In.
  7. Backend detects the tenant mismatch and, because the user confirmed linking, creates a new User in Company B, links it to the GlobalIdentity, and shares the existing wallet address.
  8. Login succeeds. A confirmation popup appears: "Account successfully linked to Company B."
  9. User can now explore Company B's Member App.
  1. User opens Company B's URL while an SSO session cookie is still active from Company A.
  2. SSOProvider automatically calls GET /api/auth/sso/check and detects the existing identity.
  3. If already linked and active: auto-login via token exchange (no popup).
  4. If not yet linked or new profile needed: the same popup from Scenario A appears automatically (without needing to enter an email).
  5. If the user clicks Yes, they are redirected to Company B's Login page. The remaining flow is the same as Scenario A from step 6 onward.

Scenario C — Direct Login at Company B (no sign up)

  1. User goes directly to Company B's Login page and enters the credentials from Company A.
  2. Backend finds the user in Company A, validates the password, and detects the tenant mismatch.
  3. Backend returns a crossTenantRequired response with the source company name.
  4. A popup appears: "You have already registered in Company A using this account. Would you like to link your account to Company B?"
  5. If the user clicks Yes, the login request is re-sent with crossTenantLink: true.
  6. Backend creates the user in Company B, links to GlobalIdentity, shares the wallet.
  7. Login succeeds. A confirmation popup appears: "Account successfully linked to Company B."
  8. User can now explore Company B's Member App.

Scenario E — Sub-company member on parent company URL (same tenant)

  1. User registered only at Company A / Branch X opens Company A's parent Member App login URL (no sub-company segment).
  2. Backend may return parentPortalLinkRequired with sourceSubCompanyName, or resolve a previously linked parent-level profile if one exists.
  3. User confirms linking; the client retries with crossParentPortalLink: true.
  4. Backend creates or links a tenant-level user (subCompanyId null), links GlobalIdentity, and returns JWTs for the parent portal.

Scenario D — Direct Login at Sub-Company (same tenant)

  1. User registered at Company A's parent portal goes to Company A / Branch X's Login page and enters their credentials.
  2. Backend finds the user in Company A parent, validates the password, and detects a sub-company mismatch (same tenant, different sub-company).
  3. Backend returns a crossSubCompanyRequired response with the source sub-company name.
  4. A popup appears: "You have already registered in Company A. Would you like to link your account to Branch X?"
  5. If the user clicks Yes, the login request is re-sent with crossSubCompanyLink: true.
  6. Backend creates a new User in Company A with subCompanyId set to Branch X, links it to the same GlobalIdentity, and shares the wallet.
  7. Login succeeds.
  8. User can now explore Branch X's Member App.
mermaid
flowchart TD
    subgraph scenarioA ["Scenario A: Sign Up (no cookie)"]
        A1["User enters email at Company B Sign Up"] --> A2["API: POST /auth/sso/check-identifier"]
        A2 --> A3{"Exists in other tenant?"}
        A3 -->|Yes| A4["Show popup: 'Registered in Company A'"]
        A4 -->|Yes| A5["Navigate to Company B Login"]
        A5 --> A6["User enters credentials, clicks Login"]
        A6 --> A7["API: POST /auth/login with crossTenantLink=true"]
        A7 --> A8["Backend creates User, links identity, shares wallet"]
        A8 --> A9["Show confirmation: 'Account linked'"]
        A3 -->|No| A10["Continue normal registration"]
    end

    subgraph scenarioB ["Scenario B: Cookie Available"]
        B1["User visits Company B"] --> B2["SSOProvider: GET /auth/sso/check"]
        B2 --> B3{"SSO session found?"}
        B3 -->|"Already linked"| B7["Auto SSO login"]
        B3 -->|"Not linked / new"| B4["Show popup automatically"]
        B4 -->|Yes| B5["Navigate to Company B Login"]
        B5 --> B6["Same as Scenario A step 6 onward"]
    end

    subgraph scenarioC ["Scenario C: Direct Login"]
        C1["User enters Company A creds at Company B Login"] --> C2["API: POST /auth/login"]
        C2 --> C3{"Tenant mismatch?"}
        C3 -->|Yes| C4["Return crossTenantRequired"]
        C4 --> C5["Show popup: 'Registered in Company A'"]
        C5 -->|Yes| C6["Re-send login with crossTenantLink=true"]
        C6 --> C7["Same as Scenario A step 8 onward"]
    end

    subgraph scenarioD ["Scenario D: Cross-Sub-Company Login"]
        D1["User enters parent creds at Branch X Login"] --> D2["API: POST /auth/login"]
        D2 --> D3{"Same tenant, sub-company mismatch?"}
        D3 -->|Yes| D4["Return crossSubCompanyRequired"]
        D4 --> D5["Show popup: 'Registered in parent company'"]
        D5 -->|Yes| D6["Re-send login with crossSubCompanyLink=true"]
        D6 --> D7["Backend creates User in Branch X, links identity, shares wallet"]
        D7 --> D8["Login success"]
    end

3. Account Switching

After a user has linked their account to multiple companies or sub-companies, there are two ways to switch:

Quick Switch from Home Page

  1. On the Home page, tap your profile avatar in the top-left corner.
  2. A bottom sheet appears showing all linked portals (companies and sub-companies).
  3. Sub-company entries are displayed as "Company Name — Sub-Company Name".
  4. The current portal is highlighted with a "Current" badge.
  5. Tap any other portal to switch instantly.
  6. The app navigates to that portal's URL and auto-logs in via SSO.

Switch Indicator

When multiple accounts are linked, a small switch icon appears on the profile avatar to indicate quick switching is available.

Full Linked Accounts Page

  1. Navigate to Account page in the Member App.
  2. Tap Linked Accounts to see all linked portals.
  3. Each entry shows the company name (and sub-company name if applicable).
  4. Tap the Switch button on any non-current portal.
  5. The app navigates to that portal's URL (e.g. /{company-slug}/ or /{company-slug}/{sub-company-slug}/).
  6. SSOProvider detects the cookie, finds an already-linked account, and auto-logs in.
  7. User is now in the other portal's app with its branding and data.

From this page, you can also unlink accounts if you no longer want them connected.

Wallet Sharing

When a user links across portals (tenants or sub-companies), all linked User records share the same blockchain wallet address. This means:

  • Tokens earned in any linked portal go to the same wallet.
  • The wallet is only created once (during the first registration).
  • The wallet is only created once (during the first registration).
  • When linking to an additional portal, the system reuses the existing wallet address instead of creating a new one.

Data Isolation

SSO shares the wallet address and credentials across portals. All other business data remains strictly isolated per portal:

  • Member profiles (separate per company or sub-company)
  • Token balances, vouchers, transactions, and campaigns
  • Push notification subscriptions and preferences
  • Store associations and membership tiers

Each portal's data is kept separate even when accounts are linked.

For integrators

Full API request/response details for SSO endpoints are documented in the External API Reference.

Programmatic Cross-Tenant Linking (External API)

In addition to the interactive Member App SSO flows described above, the External API provides a programmatic way to create users with automatic cross-tenant linking:

POST /api/external/v1/users/find-or-create

This idempotent endpoint handles three scenarios:

  1. User exists in this portal → returns the existing user (created: false, linked: false)
  2. User exists in another tenant → creates a linked profile in the current portal, sharing display name, avatar, and wallet (created: true, linked: true)
  3. Brand new user → creates a new user and GlobalIdentity (created: true, linked: false)

This is the recommended approach when syncing users from external systems (CRM, POS, etc.) — it safely handles cross-tenant identity linking without requiring the interactive SSO consent flow.

See the External API Reference for full request/response details.

Security Considerations

  • Account linking always requires the member to confirm before credentials are shared to a new portal.
  • Password changes apply across all linked portals that share the same global identity.
  • Members cannot unlink their last remaining linked portal.

VIO v4 Platform Documentation