# Tenancy and multi-entity

> A Xero tenant and a QuickBooks realm are each one company, full stop. A NetSuite account holds a subsidiary tree with elimination and consolidated rates built in. A group spanning both is the normal case, and only one of them will help you.

Source: https://lopifi.com/apis/tenancy
Part of: Lopifi — US GAAP books on autopilot, reviewed by a CPA (https://lopifi.com)

---

**What is the boundary of one set of books?** Model the entity tree in your own schema regardless of source. NetSuite's subsidiary is a mapping into it, never the tree itself.

## Divergences between Xero, QuickBooks Online and NetSuite

### QuickBooks ids are not globally unique

Ids are unique per entity type within a realm. Invoice 145 and Bill 145 are different documents. Any cache key, URL or React key built from the id alone will collide, and the collision looks like data corruption rather than a key bug.

**Handling:** Key everything as {system}:{tenantId}:{entityType}:{id}. Make the canonical id a branded string so a bare source id cannot be assigned to it.

### Consolidation is your job on two of the three

A group on Xero or QBO is n disconnected tenants. You own the entity tree, the intercompany elimination and the currency translation. On NetSuite all three exist already, and ignoring them produces double-counted intercompany revenue.

**Handling:** Own the entity tree in your schema. Map NetSuite subsidiaries into it rather than adopting them as the tree, so a mixed group — two Xero orgs plus a NetSuite subsidiary, which is common — still has a coherent parent.

### Xero switches tenant with a header, and it is easy to get wrong

The same access token serves every connected organisation; the Xero-Tenant-Id header decides which one you are talking to. A misthreaded header writes an invoice into the wrong company's books, and the API cannot tell you that you meant otherwise.

**Handling:** Never carry the tenant id in ambient context. Pass it explicitly through every adapter call, and assert it against the record's expected tenant before any write.

## Canonical shape

```ts
/** Branded, so a bare source id cannot be passed where a canonical one belongs. */
type LedgerId = string & { readonly __ledgerId: unique symbol };

const ledgerId = (s: SourceRef): LedgerId =>
  `${s.system}:${s.tenantId}:${s.entityType}:${s.id}` as LedgerId;

type Entity = {
  id: string;
  name: string;
  parentId?: string;           // our tree, not the source's
  baseCurrency: string;
  /** How this entity's books are reached. One entity, one connection. */
  connection: { system: SystemId; tenantId: string; subsidiaryId?: string };
  eliminates?: boolean;        // NetSuite elimination subsidiary
};
```
