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

# Auth, Tenancy & Actors

> Three separate concerns that meet in one call — who you are, which organisation you are acting in, and who gets blamed for the write

# Auth, Tenancy & Actors

Three questions, three mechanisms:

| Question              | Mechanism                            |
| --------------------- | ------------------------------------ |
| Who is this?          | Auth0 session                        |
| Which organisation?   | `org_id` on 81 of 98 models          |
| Who made this change? | `actors`, related to 94 of 98 models |

They meet in `getOrganizationAndActorId()`, which returns the second and third together.

## Identity

```mermaid theme={null}
flowchart TB
    User["Organisation user"]
    Customer["Portal customer"]
    Staff["CONA staff"]

    Proxy["webapp proxy.ts<br/>Auth0 session required"]
    PortalGate["portal<br/>share token + PLZ<br/>signed session cookie"]
    ConsoleGate["console<br/>requireSuperadmin"]

    Auth0["Auth0"]
    Session["getOrganizationAndActorId()<br/>auth.ts:126 · 134 files"]

    User --> Proxy --> Auth0
    Staff --> ConsoleGate --> Auth0
    Customer --> PortalGate
    Proxy --> Session

    classDef app fill:#dbe2fb,stroke:#3B56C5,color:#111827
    classDef external fill:#fff4dd,stroke:#c98a12,color:#111827
    classDef actor fill:#eceff4,stroke:#64748b,color:#111827

    class Proxy,PortalGate,ConsoleGate,Session app
    class Auth0 external
    class User,Customer,Staff actor
```

**Portal does not use Auth0.** It is the one surface with its own scheme — share token
plus postal-code verification, then a signed cookie bound to that token. See
[Portal](/architecture/portal-architecture).

**Console has a second gate.** `requireAuth()` checks the session; `requireSuperadmin()`
additionally checks `isConsoleSuperadmin(subject)`.

| Helper                        | File                              | Files using it |
| ----------------------------- | --------------------------------- | -------------: |
| `getOrganizationId()`         | `webapp app/lib/auth/auth.ts:94`  |            223 |
| `getOrganizationAndActorId()` | `:126`                            |            134 |
| `requireAuth()`               | `:199`                            |             40 |
| `getAuth0Session()`           | `:28`                             |             32 |
| `requireSuperadmin()`         | `console app/lib/auth/auth.ts:43` |   console only |

## Tenancy

```mermaid theme={null}
flowchart LR
    subgraph Direct["Direct org_id — 81 models"]
        D1["documents · general_ledger · entities<br/>items · integrations · …"]
    end

    Odd["csv_import_batches<br/>uses organization_id"]

    subgraph Transitive["Transitive — 4 models, need a JOIN"]
        T1["csv_import_records → csv_import_batches"]
        T2["document_relations → documents"]
        T3["inventory_item_history → inventory_items"]
        T4["revenue_recognition_entries → deferred_revenue_schedules"]
    end

    subgraph Global["Global — 10 models, no tenant"]
        G1["eu_vat_* · exchange_rates<br/>product_tax_* · app_store_listings"]
    end

    subgraph Roots["Roots — 2 models"]
        R1["organization · users"]
    end

    Org[("organization")]
    Org --> D1
    Org --> Odd
    D1 --> T1
    D1 --> T2
    D1 --> T3
    D1 --> T4

    classDef boundary fill:#f8fafc,stroke:#94a3b8,color:#334155

    classDef data fill:#fde8e8,stroke:#c53b3b,color:#111827
    classDef external fill:#fff4dd,stroke:#c98a12,color:#111827
    class D1,Org,R1 data
    class Odd,T1,T2,T3,T4,G1 external
    class Direct,Transitive,Global,Roots boundary
```

<Warning>
  Two ways to miss a tenant filter:

  1. **`csv_import_batches` uses `organization_id`**, not `org_id`. Tooling keyed on
     `org_id` skips it.
  2. **Four tables have no tenancy column at all** and are scoped only through a parent FK.
     A direct `WHERE org_id = …` is impossible; the query must join.
</Warning>

**Console is cross-tenant by design** — it reads across organisations and holds an Auth0
`ManagementClient`. Treat it as a stated exception, not a violation. See
[Console](/architecture/console-architecture).

## Actors

Every mutation is attributed. `actors` relates to **94 of 98 models**, almost always as
paired `*_modified_by` / `*_deleted_by` relations.

```mermaid theme={null}
flowchart TB
    subgraph Kinds["An actor is one of"]
        Human["a user<br/>user_id set"]
        App["an installed app<br/>apps_label set"]
        System["the system<br/>findOrCreateSystemActor"]
    end

    Resolve["findOrCreateActor<br/>actors/find-or-create-actor.ts:12"]
    SystemResolve["findOrCreateSystemActor<br/>find-or-create-system-actor.ts:17"]
    Actor[("actors<br/>org_id · user_id<br/>label · apps_label")]
    Mutation["any write"]

    Human --> Resolve
    App --> Resolve
    System --> SystemResolve
    Resolve --> Actor
    SystemResolve --> Actor
    Actor -->|"actorId required"| Mutation

    classDef boundary fill:#f8fafc,stroke:#94a3b8,color:#334155

    classDef pkg fill:#e8eafd,stroke:#4967E6,color:#111827
    classDef data fill:#fde8e8,stroke:#c53b3b,color:#111827
    classDef actor fill:#eceff4,stroke:#64748b,color:#111827
    class Resolve,SystemResolve pkg
    class Actor,Mutation data
    class Human,App,System actor
    class Kinds boundary
```

Actors are **per-organisation** — `org_id` is non-null, so the same user acting in two
organisations has two actor rows.

Integration-created records get an actor labelled from the integration's `ACTOR_LABEL`
constant — which is why even the placeholder integrations define one.

### Where attribution is enforced

* **Activities** — `actors` is the single most-imported core domain from the activity
  layer (21 imports), ahead of `documents`.
* **Core** — `actors` has an in-degree of 13 inside core.
* **Accounting** — `createAccountingImpact` calls `findOrCreateSystemActor` at
  `create-accounting-impact.ts:278`, **before** any write.

## Audit trails

Three separate tables, for different things:

| Table                             | Records                                                            |
| --------------------------------- | ------------------------------------------------------------------ |
| `activity_logs`                   | user-visible activity feed, 35 fields, 11 relations                |
| `background_activities`           | long-running job progress                                          |
| `copilot_actions`, `copilot_runs` | agent actions — see [Copilot Agents](/architecture/copilot-agents) |

Security events go to a separate Axiom dataset, not the database — `/admin` and `/dev`
access is logged from `proxy.ts:251-259`. See [Observability](/architecture/observability).

## Notes

**`getOrganizationAndActorId()` is the pattern to copy.** Returning both values from one
call is what stops a developer scoping a query correctly but attributing the write to
nobody.

**API keys are a fourth identity path.** `api_keys` backs the partner API routes; Mirakl
credentials are read through `mirakl/get-api-credentials.ts`.

**MFA is managed, not enforced here.** 14 files under `webapp app/lib/auth/` handle Auth0
user management including MFA enrolment, TOTP deletion, and password changes — the
enforcement lives in Auth0, not in application code.
