Account-Scoping Migration Guide

Multi-account model & company switcher

TL;DR: Users can now belong to multiple accounts. The frontend sends X-Active-Account: <slug> to tell the backend which account the user is acting as. Backend code must never assume a user has a single account — always use authorized_user from GraphQL context.

What Changed

Multi-account users

A User can have multiple UserProfile memberships across different Accounts. A user can be a broker on Company A and a borrower on Company B.

Per-request account scoping

The active account is determined per-request via the X-Active-Account header (an account slug). No more global assumptions.

Deprecations

user.default_user_profile is removed. user.role (UserRole enum) is deprecated for access-control branching. Account.account_type is deprecated for role checks.

Request Lifecycle

Every request now follows this flow:

Frontend Sends
Authorization:Bearer <jwt>
X-Origin-App:borrower | lev-internal-user-app | ...
X-Active-Account:acme-capitalNew
Backend Resolves
1JWT User record
2X-Active-Account slug Account UserProfile (membership)
3Account type AuthorizedRole
4Builds AuthorizedUser(id, role, membership, account_user) — membership resolved from X-Active-Account header
AuthorizedRole Values
admin / lev_internal_user

Internal Lev staff. Unchanged.

guest

External user with zero active memberships (no accounts).

unscoped

External user with 1+ memberships but no X-Active-Account header — hasn't picked a workspace yet.

soft_auth_lender

Lender via soft-auth token (lender portal, no full Auth0 account). Deprecating soon.

public_vault_viewer

Unauthenticated vault access via X-Vault-Hash. Leave alone.

apikey / external_client

API key callers and M2M Auth0 client credentials. Leave alone.

External scoped roles — convergingTreated identically by the platform

These three exist today as separate values but are converging into one. No logic should branch on which of these a user has.

borrower
external_broker
lender

On branch account-memberships, not yet merged to master: X-Active-Account header, authorized_user.membership, authorized_user.account_id, guest, and unscoped are all implemented and ready. Write new code toward this model now and migrate old user.role branching as you touch files.

Deal Brokered Status

Deal.is_brokered is now purely structural — based on whether the sponsor org differs from the owner org. Account type no longer determines deal type.

@property
def is_brokered(self) -> bool:
    """A deal is brokered when the sponsor org differs from the owner org."""
    if not self.owner_account or not self.sponsor_private_company:
        return False
    return self.owner_account.org_id != self.sponsor_private_company.org_id

A deal without a sponsor is never brokered (even if the account type is brokerage).

Deal user roles (broker vs borrower) are derived from deal.is_brokered.