# Federation contract — account ↔ mathapi (FROZEN)

Single source of truth for cross-app identity. Both apps build to this. Do not deviate without changing this file. Supersedes all prior key assumptions — in particular, `external_id` is **not** account's internal `users.id`.

## Roles
- **account** — identity authority, SSO, **token issuer**, School of Record, front door.
- **mathapi** — math product; attainment / compliance / condition-tags data API; tenancy / entitlement enforcement. **Relying party** (validates account's tokens).

## Join keys — immutable UUIDs only

| Key | Value | Source of truth |
|---|---|---|
| `student_ref` | account `users.public_id` (UUID) | account |
| `org_ref` | account org `public_id` (UUID) | account (thin org registry) |

Never internal auto-increment ids. Never mutable slugs. Set once, never changed.

## Token — account issues, mathapi validates
The SSO's only native org is **AGS**; it does not carry licensee-org membership. So:
1. account authenticates the user into AGS.
2. account resolves the user's licensee org + role from its app-layer **user↔org membership**.
3. account issues a token carrying: `sub` = account user `public_id` (= `student_ref`); `org_ref` = account org `public_id`; `role`.
4. mathapi validates account's token, reads `org_ref` from the claim, scopes to it. **Client-supplied org id is rejected.** The org claim is account-populated, not SSO-directory-native.

## Source-of-truth split
- Identity — user, org registry, user↔org membership, role → **account**.
- Org tenancy / seats / entitlement → **mathapi**, keyed to `org_ref`.
- Attainment, compliance eval, condition tags → **mathapi** (data API).
- Certified records, transcripts, corrections, credentials, oversight → **account** (SoR).
- Reads are one-directional: **account → mathapi**. Certified/corrections account-local; attainment mathapi-local; never duplicated.

## Endpoints (mathapi exposes; account consumes)
- `GET /api/licensee/dashboard` → attainment (cards, at-risk A/B/C, level distribution, roster) + org compliance (`eligible | below_floor` + failing params) + per-learner condition tags. Org-scoped from `org_ref`. Keyed on `org_ref` + `student_ref`.
- `GET /api/licensor/portfolio` → tenants keyed on `org_ref` + `below_floor` flag. AGS-only.

## Gates (populate the keys — mostly NULL today)
1. **SSO federation** — account issues tokens, mathapi validates (confirm mechanism against account's stack).
2. **Org reconciliation** — populate account org registry + `mathapi.organizations.external_id = account org public_id`. Map existing orgs.
3. **Student mapping** — populate `mathapi.users.external_id = account user public_id`. Map existing math users to account students.

## Integration gate — neither side is done alone
End-to-end: account issues a token → mathapi validates → `org_ref` resolves → the org-scoped API returns the correct org's data and ZERO rows from any other org, under the federated token. Both sides pass this jointly before any consumer (the portal) wires in.

## Retired (do not reintroduce)
- `mathapi.users.external_id = account internal users.id` — **retired**. It now holds account user `public_id` (UUID).
- account's `external_id == users.id` downstream contract — **retired** in favor of `public_id`.

---
_Source: authored in `account` (SoR); mirrored here for mathapi contributors. account is canonical if they diverge._
