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

# Multi-tenancy

> Organizations, users and employees from an integration's point of view — and what tenant isolation guarantees you.

## The three entities

| Entity           | Is                                         | Identified by                                                           |
| ---------------- | ------------------------------------------ | ----------------------------------------------------------------------- |
| **Organization** | A tenant. One company's data.              | A UUID. Never appears in your requests — it comes from your credential. |
| **User**         | The person. One login.                     | A globally unique email.                                                |
| **Employee**     | A person's membership in one organization. | A UUID, plus a `code` unique **within** the organization.               |

The distinction matters for integrations in one specific way: **`code` is unique per organization, `email` is unique globally.** Reconciling against an external system by employee code is safe within one organization and ambiguous across several.

## Your key is org-scoped

An API key belongs to an organization. Every request under it is confined to that organization's data, and there is no parameter that changes which organization you are reading.

```mermaid theme={null}
flowchart LR
  K[API key] --> O[Organization]
  O --> E[Employees]
  O --> A[Attendance]
  O --> L[Locations, departments, roles]
```

To integrate with two organizations, hold two keys.

## Isolation is enforced by the database

Tenant scoping is not a convention that every query is expected to remember. It is enforced by row-level security in PostgreSQL: every request runs inside an organization context, and **a query with no context returns zero rows** rather than everything.

Two consequences you can rely on:

<CardGroup cols={2}>
  <Card title="Cross-org reads return nothing" icon="shield">
    Not "forbidden" — nothing. The rows are not visible to your session at all.
  </Card>

  <Card title="A bug fails closed" icon="lock">
    A missing organization context produces an empty result or a rejected write, never a leak.
  </Card>
</CardGroup>

## What that means for your error handling

<Warning>
  **A `404` does not prove an ID is invalid.** An ID belonging to another organization produces exactly the same `404` as one that does not exist anywhere.

  This is deliberate: distinguishing them would confirm the record exists. Do not build logic that treats `404` as "this ID is malformed" — treat it as "not available to this credential".
</Warning>

## Identifiers in practice

<Steps>
  <Step title="Store the employee UUID, not the code">
    The `id` is stable. The `code` is an organization-assigned business key that an administrator can change.
  </Step>

  <Step title="Match on code only when you must">
    Reconciling against an external HRIS usually means matching on `code`, because that is what both systems share. Do it once, then store the UUID mapping.
  </Step>

  <Step title="Never assume email identifies an employee">
    Email identifies the **person**, who may hold memberships in several organizations. Within one organization it is effectively unique, but it is not the key.
  </Step>
</Steps>

## Organization switching

A person belonging to several organizations switches between them without re-authenticating — the session changes which membership it is acting as.

<Note>
  This affects interactive sessions only. It has no bearing on API keys, which are bound to one organization and cannot switch.
</Note>
