Product System of Record
The product System of Record (SoR) is the client-facing data layer — tenant data, modeled in
@tedos/domain (Drizzle/Postgres) in a dedicated product Postgres schema. It is separate
from the dev-workflow Plan Engine graph: the two never share tables (see the
two-database split in Platform map).
It shipped in effort E1 (the "Comprender MVP slice 1"), identity-first: a single, unified person deduped across a client's channels, with fiscal data, on real infra with server-side tenancy + roles. Comprender is tenant #1 — see Comprender.
What E1 delivered
- A product SoR package —
@tedos/domain— in a dedicatedproductPostgres schema. - A frozen shared contract in
@tedos/shared(zod-as-source + inferred TypeScript types) forproject/person/person_identityplus the endpoint shapes. Both the API and the admin import it — consistency by construction. - Auth + tenancy in
apps/api(Clerk): arequireProjectguard maps a Clerk org →projectId- role; the operator is superadmin. Every
/p/:id/*route is authorized server-side.
- role; the operator is superadmin. Every
- A two-level admin shell (platform + project context) — see admin.
- Match-key dedupe (email / phone / RFC): the same person from two channels unifies into one record, with an append-only merge audit.
Data model (@tedos/domain, product schema)
| Table | Purpose |
|---|---|
project | The tenant (= a Clerk org = a client) — clerk_org_id, name, status, domain |
person | A unified person inside a project — full_name, email, phone, rfc (fiscal PII, masked at presentation) |
person_identity | One source/channel record for a person — channel, external_ref, raw (jsonb) |
person_merge | Append-only merge audit log |
Match keys — unique-ish on (project_id, email), (project_id, phone), (project_id, rfc);
the merge/dedupe service links on these. Tenant scoping (project_id) is mandatory on every
product row.
API surface (apps/api, Fastify v5)
All routes sit behind the auth/tenancy guard (Clerk org + role; superadmin = any project). Tenant scoping is server-side, never trusted from the client.
| Method | Route | What |
|---|---|---|
GET / POST | /projects | List / create the operator's projects |
GET | /projects/:id | One project |
GET | /p/:id/students | List + search the project's people |
GET | /p/:id/students/:sid | One person (single-view) |
POST / PATCH | /p/:id/students | Create / update a person |
POST | /p/:id/students/merge | Merge two people (dedupe) |
GET | /p/:id/summary | Project summary |
The admin reaches these through a server-side proxy, so the Clerk session + tenant context inject on the server — see admin.
Roles
A Clerk org membership maps to a project role: Owner · Sales · Finanzas · Soporte · Alumno. The
operator is superadmin (any project). The role gates Level-2 nav (admin) and authorization on every
/p/:id/* route (API).
Isolation: schema-per-client
E1 shipped a single product schema. The next architecture step (ADR-007) moves to
schema-per-client isolation — one Postgres cluster, a schema per project, a control/registry
schema for the project list, and a tenant-scoped DB accessor that selects the schema. This makes a
clean per-client export trivial ("clients own and can export their data"). See
Data, memory & assistant.
The hard rule
Per ADR-006, tenant/client data never enters the dev-workflow graph. The product SoR
(product schema) and the Plan Engine graph (plan schema) never share tables. As built, both
schemas currently live on the same Fly Managed Postgres cluster — a documented compromise (no real
tenant data yet) with a tracked upgrade path to a dedicated cluster before real tenant-PII volume.
See Decisions (ADR-006/007).