> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ntropii.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenant architecture

> Ntropii Workspace (control plane) versus Ntropii Tenant (data plane) — what lives where and why the split matters.

Data sovereignty and isolation is central to our architecture. Each private "tenant" is deployed in a dedicated infrastructure ensuring there is no comingling of client data.

Ntropii separates *control* from *data*. Two distinct planes with strict isolation between them:

```text theme={null}
┌─────────────────────────────────────────────────────┐
│              Ntropii Workspace (you)                │
│                                                     │
│   - Tenant + entity registry                        │
│   - Workflow definitions and versions               │
│   - Deployment + schedule metadata                  │
│   - Run status / events (no payloads)               │
│   - API keys, organisation, users                   │
│                                                     │
│   Reached via:  CLI   |   MCP   |   SDK             │
└────────────────────────┬────────────────────────────┘
                         │
                command + status events only
                  (no financial data)
                         │
┌────────────────────────▼────────────────────────────┐
│             Ntropii Tenant (per client)             │
│                                                     │
│   - Runbook execution (Temporal workers)            │
│   - Data platform credentials                       │
│   - Document storage                                │
│   - Direct access to your warehouse                 │
│                                                     │
│   Bound to:  Managed Postgres  |  Snowflake  |       │
│              Microsoft Fabric                       │
└─────────────────────────────────────────────────────┘
```

## The boundary

**Ntropii Workspace** is what you reach when you run `ntro tenant create` or call an MCP tool from an LLM. It holds metadata: who owns which tenant, which entities (SPVs, funds) belong to which tenant, which workflow versions have been pushed, which deployments are active, which scheduled runs are coming up.

It does *not* hold any financial data. No journal lines, no trial balances, no extracted documents. Those never leave Ntropii Tenant.

**Ntropii Tenant** is the runtime that actually executes your runbooks. One tenant binds to exactly one data platform — Ntropii's managed Postgres (provisioned per tenant, isolated from every other tenant), a Snowflake account, or a Microsoft Fabric workspace. The credentials for the chosen platform live inside Ntropii Tenant; they never cross to Workspace.

Every tenant gets its own isolated infrastructure regardless of which strategy it picks. The boundary is the *tenant*; what differs is who hosts the platform underneath. With managed Postgres, Ntropii hosts the database and isolates per tenant. With BYO (Snowflake, Microsoft Fabric), the customer hosts the platform and Ntropii holds only the credentials needed to read from it.

When a workflow runs, it executes inside Ntropii Tenant against the chosen data platform. Status events (started, step completed, run finished) flow back to Workspace so you can monitor in the CLI / UI / MCP. The actual data — the documents, the trial balance, the journal entries — stays in the data platform.

## Tenants and entities

A service provider (e.g. accounting firm, fund administrator, transfer agent, multi family office) or asset manager would signup as an organisation creating a **workspace**. "Fund Admin Workspace" is the organisation name in our example below who has a workspace.

A **tenant** is a client of the workspace's organisation — typically a fund. Acme Fund is a tenant. They map 1:1 to a data platform binding.

An **entity** is a company, SPV, protected cell or incorporated cell within a tenant. A tenant can have many entities — each with its own schema in the underlying data platform, its own scheduled workflows, its own task history. "Acme Commercial SPV 1" is an entity belonging to the Acme Fund tenant, sharing Acme's data platform but living in its own schema.

```text theme={null}
Fund Admin Workspace
└── Tenant: Acme Fund
    │   (data-platform: managed-postgres)
    ├── Entity: Acme Commercial SPV 1   → schema: acme_spv1
    ├── Entity: Acme Commercial SPV 2   → schema: acme_spv2
    └── Entity: Acme Infrastructure     → schema: acme_infra
```

Workflows are registered at the tenant level, deployed to Ntropii Tenant, and triggered against an entity for a specific period.

## Why the split

Three reasons, all rooted in the regulated nature of fund operations:

<AccordionGroup>
  <Accordion title="Data sovereignty">
    Customer financial data — invoices, bank statements, GL entries, NAV history — never enters Ntropii Workspace. It stays in Ntropii Tenant. With managed Postgres that database is hosted by Ntropii but isolated to one tenant; with BYO (Snowflake, Microsoft Fabric) the data stays in the customer's own warehouse under their own credentials and retention policies. If a customer's compliance team requires that no fund data crosses a particular border, the BYO route deploys Ntropii Tenant in their own region (or their own cloud account) without changing the architecture.
  </Accordion>

  <Accordion title="Tenant isolation">
    Each tenant has its own runtime, its own credentials, and its own data platform binding. A bug in one tenant's runbook can't leak data to another tenant — there's no shared execution surface. API keys are scoped to a tenant (the organisation is recorded only as provenance), so a leaked key affects exactly one client's surface area.
  </Accordion>

  <Accordion title="Auditability">
    The control plane has a clean audit trail of *what was deployed when*. The data plane has a clean audit trail of *what was processed when*. They join on workflow ID + run ID. An auditor reconstructing a NAV from six months ago can pull the runbook source from Workspace at that commit, the input documents from the customer's data platform at that timestamp, and re-run the flow to verify the output bit-for-bit.
  </Accordion>
</AccordionGroup>

## What this means for you

You'll see this split in three concrete places:

* **`ntro tenant create --data-platform <choice>`** declares the data-platform strategy. With `managed-postgres`, that's all there is — Ntropii provisions and isolates the database. With `snowflake` or `microsoft-fabric`, the tenant binds to a config registered separately via `ntro integration add`, which is the moment Ntropii Tenant gets the customer credentials.
* **`ntro workflow create --path ./runbooks/<slug>/`** uploads the runbook bundle to Ntropii Tenant's worker, where it can execute against the data platform.
* **`ntro workflow create --entity <entity>`** (same command, with `--entity`) also creates a workflow binding so the runbook can be triggered for that specific entity.

## Data lifecycle: from agent input to GL

Inside a tenant, data tightens through four stages on its way from a fuzzy agent submission to an immutable general-ledger entry. Each stage has its own type strictness, its own storage location, and its own reviewer. By the time a value reaches stage 4, every prior stage's invariants are encoded in the type — you cannot post an unbalanced journal because the type system has already eliminated invalid states.

This is the regulatory contract: "parse, don't validate" is what makes a NAV reproducible from the source code plus its inputs, six months after the fact, bit-for-bit. JFSC, CSSF, FSRA, and Big-4 auditors all want the same thing — a paper trail showing exactly which checks ran when. The lifecycle below is what materialises that trail in code rather than policy.

```mermaid theme={null}
flowchart LR
    A["<b>Stage 1</b><br/>Agent input<br/><i>JSONB blob</i>"] --> B["<b>Stage 2</b><br/>Subledger PENDING<br/><i>Forgiving types</i>"]
    B --> C["<b>Stage 3</b><br/>Subledger APPROVED<br/><i>HITL-reviewed</i>"]
    C --> D["<b>Stage 4</b><br/>GL journal<br/><i>Immutable, balanced</i>"]
```

<AccordionGroup>
  <Accordion title="Stage 1 — Agent input (no validation)">
    A coding agent or MCP client submits a payload via `ntro_task_data_ingest`. The platform stores the raw `data` field as JSONB in `ingest.submitted_records` inside the tenant's Postgres, alongside provenance (event id, source ref, received at, kind). Nobody type-checks the payload at this stage — agents send fuzzy strings, dicts, partial fields, locale-specific dates, whatever the source document looked like. The point is preserving exactly what was submitted so an auditor can reconcile back to the wire format.
  </Accordion>

  <Accordion title="Stage 2 — Subledger PENDING (forgiving types)">
    A runbook activity reads the JSONB payload and `model_validate`s it through a Pydantic boundary model declared with `ntro.types.*` types — `ForgivingDate` for fuzzy dates, eventually `ForgivingMoney` / `Currency` siblings as runbooks need them. These types coerce ISO strings, locale-tolerant strings, and natural language into typed values; on parse failure they degrade to `None` rather than raising. The activity then writes the typed values to a subledger (e.g. `ledgers.expenses`) with `status=PENDING`. This is the first rung where the data is typed; rows are still allowed to have NULLs because the next stage's reviewer fills them in.
  </Accordion>

  <Accordion title="Stage 3 — Subledger APPROVED (HITL review)">
    A human reviewer sees every PENDING row in a `DATA_TABLE` HITL step — typically a fund accountant during the period close. They correct mis-categorised vendors, fill in missing dates, flag ambiguous amounts, and approve the row. On approval the SDK flips status `PENDING → APPROVED`. Required fields are now present; lifecycle invariants hold. Nothing reaches the GL without passing through this gate.
  </Accordion>

  <Accordion title="Stage 4 — GL journal (immutable, balanced)">
    A separate runbook step (or a downstream workflow) rolls up APPROVED subledger rows into balanced journal proposals and posts them to the customer's general ledger — Xero, QBO, or whichever GL the tenant binds to. The GL provider rejects anything that doesn't balance, doesn't reference a valid CoA code, or duplicates an existing posting (idempotent by external ref). Once posted, the row is immutable; corrections are issued as offsetting journals, never as updates.
  </Accordion>
</AccordionGroup>

The point of the ladder is that the **type carries proof of every check that has cleared so far**. By the time a `JournalLine` is constructed, its existence is evidence that the agent submission parsed cleanly, the reviewer signed off, and the CoA code is valid. That's the regulator's audit trail rendered as types — not a separate compliance artefact, but the same code that runs in production.

The next page walks through both setup steps.

<CardGroup cols={2}>
  <Card title="Configure environment" icon="gear" href="/get-started/configure-environment">
    Bind a data platform and set up your runbook repo.
  </Card>

  <Card title="API keys" icon="key" href="/get-started/api-keys">
    Generate, scope, and rotate keys for CLI / MCP / SDK access.
  </Card>
</CardGroup>
