# API Contract

The single source of truth for the public API contract is
[`openapi/v1/openapi.json`](../../openapi/v1/openapi.json).
This document is the *human* explanation of the rules behind that spec.

## What's in the contract

`/api/v1/*` and only `/api/v1/*`.

- These routes are versioned, documented, and held to a backwards-compatibility
  bar.
- The OpenAPI spec is the authoritative description; the route handler is the
  authoritative implementation. PRs that change one without the other fail CI.

## What's NOT in the contract

Everything else under `app/api/`. These routes ship and change with the
internal product; consumers outside this codebase should not depend on them.

| Path prefix | Audience | Reason for exclusion |
|---|---|---|
| `/api/admin/*` | Tenant admin UI | Internal admin panel; auth/permissions tightly coupled to the UI. |
| `/api/owner/*` | Platform owner / SaaS operator | Multi-tenant management API; uses owner-role auth, not tenant auth. |
| `/api/demo/*` | Demo packs UI | Tooling. |
| `/api/planning/*`, `/api/wertstrom/*`, `/api/repository/*`, `/api/qaf-template`, `/api/projects/next-id`, `/api/holidays/seed` | First-party UI XHR | Not yet stable enough to publish. Promote to `/api/v1/*` before exposing. |

## Versioning rules

- The version lives in the URL path (`/api/v1`, `/api/v2`, …).
- **Additive changes** — new endpoints, new optional query params, new optional
  response fields — stay in the current major version.
- **Breaking changes** — renames, type narrowing, removed fields, semantic
  shifts — require a new major version. Both versions ship side-by-side until
  the older one's deprecation window closes.
- We have not deprecated anything yet. When we do, the spec will gain a
  `deprecated: true` flag on the operation and a `Sunset` header on the
  response.

## Response envelope

Every v1 response uses one of three shapes (helpers in `@/lib/api`):

```ts
// success — single resource
{ "data": { ... } }                                // ok(data)

// success — list with pagination
{ "data": [...], "meta": { "page", "limit", "total" } }   // list(data, meta)

// error
{ "error": "human message", "code": "...", "details": { ... } }  // err / unauthorized / forbidden / ...
```

`code` enums (defined in `lib/api/envelope.ts`):
`unauthorized`, `forbidden`, `not_found`, `invalid_request`, `tenant_disabled`,
`plan_missing`, `role_denied`, `kill_switch`, `service_misconfigured`,
`internal_error`.

`code` is optional today (legacy routes don't all set it). It is **required
for new routes**.

### Schema strictness

Resource schemas in `openapi/v1/openapi.json` fall into two camps today:

| Schema | `additionalProperties` | Reason |
|---|---|---|
| `Supplier`, `SupplierCreate`, `MasterDataValue`, `HealthResponse`, `ErrorEnvelope`, `ListMeta` | `false` (closed) | Shape verified against the current SQL schema or the envelope helpers. |
| `Project`, `ProjectCreate`, `ProjectPatch` | `true` (open) | PATCH/POST return the full projects row with 25+ MO-24 columns whose surface is still moving. Tighten when MO-24 stabilises. |
| `OeeRecord`, `OeeRecordCreate`, `Assignment` | `true` (open) | Route handlers reference columns that do not match the current DB schema. Tracked in [api-surface.md](./api-surface.md#known-schema-vs-route-mismatches-v1) and [follow-ups.md](./follow-ups.md) #8. |

Closed schemas REJECT unknown properties on validation. Open schemas TOLERATE
them. The rule for new schemas: start closed; only open if the underlying row
is genuinely unstable.

## Authentication

| Scheme | Used by | Source |
|---|---|---|
| `cookieAuth` | Default for all `/api/v1/*` | Supabase session cookie, refreshed by `lib/supabase/proxy.ts`. |
| `supportSession` | Bypass for support engineers | `X-Support-Session-Id` header, validated by `lib/support`. |

Anonymous endpoints (e.g. `/api/v1/health`) override with `security: []` in
the OpenAPI operation.

## Internal vs external types

This is the conceptual separation today:

| Layer | Lives in | Stability |
|---|---|---|
| **Internal TypeScript types** (DB row shapes, internal service contracts) | `lib/<module>/types.ts` (not re-exported through the barrel unless used externally) | May change with the codebase. |
| **Module DTOs** (cross-module data shapes) | `lib/<module>/index.ts` (re-exported) | Treated as internal API; breaking changes searched-for in the same PR. |
| **External API request/response schemas** | `openapi/v1/openapi.json` (`components.schemas.*`) | Versioned. Mirror of `lib/api/envelope.ts` for shared envelopes; resource schemas are hand-maintained. |

We deliberately do NOT generate one from the other today. The mapping is small
enough to maintain by hand and the cost of code-gen tooling exceeds its value
at current scale. Revisit when a second client (iOS app, BMW WebEAM bridge)
consumes the spec.

## How to add a new external endpoint

1. Implement `app/api/v1/<resource>/route.ts`.
2. Use envelope helpers from `@/lib/api`. Use the tenant/entitlement guards
   from `@/lib/tenant` and `@/lib/entitlement` (not the deep paths).
3. Update `openapi/v1/openapi.json`:
   - add the path under `paths`
   - tag it under an existing or new top-level `tags[]` entry
   - add request/response schemas under `components.schemas` (with `required`
     and field types)
4. `npm run check:openapi` must pass.
5. Mention the new route in the relevant module README.

## How to change an existing endpoint

- **Additive** — update the spec in the same PR.
- **Breaking** — don't, in v1. Add a `/v2` route alongside.
- **Behavioral change with the same shape** (e.g. tightening validation) —
  update the spec's `description` to document the new constraint; bump
  `info.version` patch number.

## How to deprecate an endpoint

- Mark the operation `deprecated: true` in the spec.
- Update the description with the migration path (the new endpoint or the
  reason for removal).
- Plan the sunset window with the consumers.
- Keep the route alive until the sunset window closes.

## Validation

- `npm run check:openapi` — structural check + path-to-route cross-check.
- `npm run check:portability` — bundles `check:openapi` along with the other
  rails so CI fails fast on contract drift.

## Out of scope (deliberately)

- GraphQL — not adopted; the OpenAPI surface covers the integration needs.
- gRPC — not adopted.
- Code generation from the spec — not adopted.
- Mock server / Prism — not adopted; revisit when a second client appears.
- Postman / Insomnia collections — derive from the spec on demand if needed.
