Skip to main content

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 dedicated product Postgres schema.
  • A frozen shared contract in @tedos/shared (zod-as-source + inferred TypeScript types) for project / person / person_identity plus the endpoint shapes. Both the API and the admin import it — consistency by construction.
  • Auth + tenancy in apps/api (Clerk): a requireProject guard maps a Clerk org → projectId
    • role; the operator is superadmin. Every /p/:id/* route is authorized server-side.
  • 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)

TablePurpose
projectThe tenant (= a Clerk org = a client) — clerk_org_id, name, status, domain
personA unified person inside a project — full_name, email, phone, rfc (fiscal PII, masked at presentation)
person_identityOne source/channel record for a person — channel, external_ref, raw (jsonb)
person_mergeAppend-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.

MethodRouteWhat
GET / POST/projectsList / create the operator's projects
GET/projects/:idOne project
GET/p/:id/studentsList + search the project's people
GET/p/:id/students/:sidOne person (single-view)
POST / PATCH/p/:id/studentsCreate / update a person
POST/p/:id/students/mergeMerge two people (dedupe)
GET/p/:id/summaryProject 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).