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

# Subject resolution

> How Naive resolves which tenant user a request acts on — and the cross-tenant guard.

Every data-plane request resolves a **subject tenant\_user** before doing anything.

## Resolution order

1. Explicit `:user_id` in the path (`/v1/users/:user_id/...`)
2. `X-Naive-User-Id` header
3. The API key's `active_tenant_user_id` (set at key creation)
4. The company's default tenant\_user (auto-created on signup)

The sentinels `default` and `me` in positions 1–2 mean "use the default user".

## Cross-tenant guard (the ballgame)

After picking a candidate user, the resolver asserts:

```
resolved_tenant_user.company_id === api_key.company_id
```

If it doesn't match, the request returns **404 — not 403**. The existence of another
company's user is itself information we never leak.

`X-Naive-User-Id` is treated as **untrusted input** — validated against the key's
company exactly like a path param, never as an identity assertion. A workspace-wide key
can target any user *in its own company*, never another company's.

Ids are also shape-checked: a malformed (non-uuid) `:user_id` / `X-Naive-User-Id` /
resource id returns a clean **404**, never a `500` from the database rejecting the uuid
cast. (`default`/`me` sentinels are exempt — they resolve before the check.)

Every handler that loads a resource by id (card, inbox, vault entry, connection, ...)
additionally passes it through `assertSameCompany(resource, req)` so a forged resource
id from another tenant 404s.

## Workspace vs kit-scoped keys

* **Workspace-wide** (`account_kit_id` null): can target any user in the company.
* **Kit-scoped** (`account_kit_id` set): locked to one AccountKit.

Both are bounded by the company. That boundary is the entire premise of tenant
isolation, so the guard runs on every request — not as an afterthought.
