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.
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.
Every request now follows this flow:
AuthorizedUser(id, role, membership, account_user) — membership resolved from X-Active-Account headeradmin / lev_internal_userInternal Lev staff. Unchanged.
guestExternal user with zero active memberships (no accounts).
unscopedExternal user with 1+ memberships but no X-Active-Account header — hasn't picked a workspace yet.
soft_auth_lenderLender via soft-auth token (lender portal, no full Auth0 account). Deprecating soon.
public_vault_viewerUnauthenticated vault access via X-Vault-Hash. Leave alone.
apikey / external_clientAPI key callers and M2M Auth0 client credentials. Leave alone.
These three exist today as separate values but are converging into one. No logic should branch on which of these a user has.
borrowerexternal_brokerlenderOn 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.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_idA 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.