Skip to main content
A project is a scope inside an organization. It owns account kits and child projects, and through them every primitive:
Projects are additive. Every organization has exactly one default project, and a call that names no project resolves to it — which is every call written before projects existed. A single-project organization behaves precisely as it did; the layer only becomes visible when you create a second project.
The rename is vocabulary at the product layer. The physical tables are unchanged: an organization is a companies row and a child project is a tenant_users row, so company_id, tenant_user_id, /v1/company and /v1/users/:user_id/... all keep working. Only projects is new.

The default project

Created for each organization by migration 059, which also backfilled every pre-existing account kit, child project, connection, vault entry, approval, activity event, and agent into it. An organization created before the migration and never touched since gets its default project lazily, on first read. The default project is not deletable and not archivable. It is where every un-projected caller lands, so removing it would strand the organization. A row whose project_id is NULL belongs to the default project. The column is nullable on purpose (nothing that writes these tables is required to know about projects yet), so a write from an un-taught path lands as NULL and is still visible exactly where its organization can see it — never invisible, never another project’s.

Selecting a project

Five ways, in this order:
  1. The path/v1/projects/:project_id/... in front of any data-plane route. /v1/projects/<id>/users/<child_id>/vault is /v1/users/<child_id>/vault scoped to that project.
  2. The X-Naive-Project-Id header — same effect without changing the route. Treated as untrusted input and validated against the key’s organization, like any path param.
  3. The API key’s pinned project (active_project_id), set when the key is created.
  4. The project of the key’s sealed child project, if it is sealed to one — what makes a key sealed into a non-default project usable without also pinning the project. A child project named in the path does not select a project this way: naming a child of another project without selecting that project is a 404, not a door into it.
  5. The organization’s default project.
The sentinels default and current mean the caller’s own project: the key’s pinned project when it has one, otherwise the organization’s default project. They are a way to decline to name a project, not a way to name the default one — a pinned key writing default still acts inside its pin.
A pinned key is authoritative, exactly as a sealed key is for its subject. A caller-supplied project id may only agree with the pin; disagreeing is a 403 key_project_mismatch, never a redirect into the named project.

The cross-project guard

Subject resolution now asserts both axes. After resolving a candidate child project, the resolver requires:
Either mismatch returns 404 — not 403. The existence of another organization’s — or another project’s — child project is itself information we do not leak. The one deliberate exception is the key-pin mismatch above, which is a 403 because it is about your own key, not about what exists. See Subject resolution for the subject half of the same resolver.

What is project-scoped, and what is organization-wide

Deployments were already namespaced by (organization, project) before this layer existed — the IaC project: string is the same project key, now first-class. See Infrastructure as code.

Using it

Everything written against the old names keeps working and resolves to the default project: naive.forUser(id), naive.users, naive.accountKits, naive use <child_project_id>, /v1/users/..., /v1/company, and MCP tools called with user_id alone. See Projects (getting started) for the walkthrough.