Skip to content

CAP data model

Audience: backend
Status: specced
Owns: backend
Depends on: Workflow abstraction, Orchestrator data model, Coding standards, Architecture handover

Implementation SoT: elimi-ecosystem/apps/cap/prisma/schema.prisma (migrated). Design seed also exists at ecosystem-root cap-schema.prisma and in reference-docs/v2/. This page is the human outline. Status fields stay String + CHECK in migration SQL (§23). New CAP-only status domains (job posting including filled, retained request, interview mode, form types) are owned by CAP — not required in @yourorg/common.

Rule Detail
ULID PKs @id @db.Char(26), app-generated
Money BigInt minor units — never Float
JSONB Only where justified: Application.data, OnboardingRecord.data, Unit.structure, qualification requirements/objectives, Evidence Vault / interview form content
Version-lock Templates / NOS / Policy / Pricing / Terms carry version; in-flight rows point at a specific version
Enums Structural only (PersonaType, ApplicationType, CentreStaffRole, AssessorQualification, SignoffRole)
Status fields String + intended CHECK — core lists may sync with @yourorg/common; newer CAP-only domains live in CAP
Audit No audit_logs table here — separate Audit service (§14)

CapUser — identity anti-corruption layer

Section titled “CapUser — identity anti-corruption layer”
Pattern Rule
One table holds Orchestrator userId CapUser.userId unique + indexed
All other CAP tables FK to CapUser.id Not to Orchestrator ids directly
Provisioning Consume user.created idempotently (dedupe on userId). Consume user.deleted to redact PII (including photoAssetId) and inactivate memberships (keep application / wallet FKs)
Model Notes
CapUser Local id + Orchestrator userId + persona (entry/historical) + identityVerified + optional photoAssetId (person-level profile photo; not in onboarding JSON). Acting hat is request header X-CAP-PERSONA, not this column. Display URLs are API-only (ResolvedAsset: assetId + url) — CAP does not persist signed URLs. Photo, centre logo, and candidate signature GETs resolve at read time; nested onboarding data stays id-only.
OnboardingRecord Unique (capUserId, persona); optional variant (staff | organisation) for centre. Staff onboarding is person-scoped.
Candidate, AssessorProfile, CentreStaff[], AwardingBodyStaff Persona profiles → CapUser. A user may be candidate and staff at one or more centres. Assessors are not CentreStaff. AssessorProfile.status pending | approved | suspended | rejected — only approved may marketplace-apply or create retained requests. Platform admin list exposes snapshot + centreCount (approved retained only). Candidate may hold defaultSignatureAssetId (§28)
Centre, AwardingBody Orgs; centre status pending | approved | suspended | rejected. Platform create starts approved + SUPER_ADMIN invite; org onboarding starts pending. Admin list DTOs expose joinedAt (= createdAt), totalCandidates (distinct candidates with ≥1 app), totalApplications (application rows), totalAssessors. Centre.address, supportContact, accountDetails JSON from organisation onboarding; exposed on GET /centre/profile (PATCH super_admin; registrationNo locked; GET also returns logoAssetId + resolved logo). Catalogue GET /centres and admin centre list/detail include the same logo pair. CentreStaff unique (capUserId, centreId); status PENDING/ACTIVE/INACTIVE
Sector, Trade, NosDocument, QualificationLevel, Unit Platform catalogue; version-locked NOS/units. Trade.status active|archived (soft-archive). Sector/trade slug auto-generated from name. Admin mutations target the active NOS (create stub if missing); import supersedes prior active versions. LOs live in Unit.structure
AssessorSectorExperience Assessor ↔ Sector M2M — marketplace eligibility basis (§27)
WorkflowTemplate, StageDefinition Config-driven stages + handlerStrategy
Application type RPL/NSQ, status, current stage, data JSONB, schemaVersion, optional receiptAssetId (§28), submittedAt (set on first submit; null while draft)
ApplicationVersion, ApplicationUnit, ApplicationWorkflowState, StageHistory History + unit M2M + workflow pointer
Feedback, Appeal, Resolution Stage-scoped cross-cutting
FacilitatorAssignment, InterviewPanel, PanelMember, InterviewSchedule, InterviewEvaluation, InterviewForm RPL panel path — InterviewSchedule has mode (physical|online), link, useCentreAddress; InterviewPanel.centreInterviewId optional provenance from a centre template; InterviewForm is one row per formType (records | assessment_grid | practical_observation | skill_demonstration); lead writes flexible JSON data via PUT …/forms/{formType} (blocked after signoff); candidate signoff → wire status completed (§28)
CentrePanel, CentrePanelMember, CentreInterview Centre templates for reusable panels/interviews (/centre/panels, /centre/interviews). Copy-on-assign via POST /centre/interviews/{id}/schedule. Centre diary GET/PATCH /centre/interview-bookings is seated InterviewSchedule (not a new table) — later template PATCH does not update seated copies
ApplicationEvent RPL calendar (general-purpose; includes applicationId for cross-app dashboard) — GET /applications/{id}/events, GET /candidate/events, GET /assessor/events (§28–29)
IvAssignment, EvAssignment IQA / EQA contextual roles
AssessorAssignment NSQ UNIT_ASSESSOR contextual role (§25) — parallel to IV/EV
Certificate issued_by, asset id, status — not created by centre certify bypass (CAP_CENTRE_CERTIFY_BYPASS)
ApplicationShareToken One hashed token per application (tokenHash SHA-256 hex, no plaintext, no TTL). Centre POST/DELETE /applications/{id}/share-token after IV APPROVED; public GET /shared/applications/{token} (+ asset proxy). Grpc mode file URLs use ORCHESTRATOR_SERVICE_API_KEY → OL POST /storage/resolve.
UnitAssessmentRecord, PerformanceCriteriaEvidence, UnitSignoff NSQ-only NOS evidence / sign-off
InductionForm NSQ-only induction (form_submission strategy)
DirectObservationSession NSQ DO (+ PRF fields as designed)
GeneralEvidence, SelfAssessment, ThirdPartyReportEvidence RPL Evidence Vault (Folder Arrangement) — application-level, not NOS unit evidence (§28)
Wallet, WalletTransaction, CentrePricing Centre finance — wallet credits on payment.completed (net of processing_fee_percentage), unique on payment paymentId / reference for idempotency. Centre payments page sums credits all-time; platform admin summary exposes fee slice (platformRevenue), Σ credits (walletDeposits), and pending gross only (pendingAmount). Admin list rows expose candidate gross + centreNet (null unless completed). CentrePricing latest-per-type, batch PUT is one transaction
CentreActivity Centre-scoped staff activity (actor, role, action, metadata, UTC occurredAt) — not the Audit service (§14)
Policy, Pricing, TermsVersion, TermsAcceptance Platform admin versioned config. Latest version per key/type is “current”. Form facade: GET/PATCH /admin/platform-config (dirty fields publish new rows with effectiveFrom=now). Direct POST /admin/policies, /admin/pricing, /admin/terms still publish versions
PlatformDocument Admin-replaceable downloadable files (key closed set, today third_party_report). Stores OL assetId; GET /admin/templates lists the catalog with resolved asset.url; PUT /admin/templates/{key} replaces. Public GET /evidence/third-party-report-template reads this row (env fallback)
JobPosting, JobPostingApplication Centre/AB-initiated postings; assessor applies (§27). Status open | filled (server-set when accepted ≥ slot) | closed (centre took it off marketplace) | cancelled. Shortlist → application accepted
RetainedAssessorRequest Assessor-initiated standing request at a centre; approved row = retained relationship (§27). Includes required preferredRole (assignment-role wire: facilitator / panelist / lead_panelist / observer / iv / unit_assessor / ev) — preference at apply time, not an actual assignment
  • AssessorAssignment, UnitAssessmentRecord, PerformanceCriteriaEvidence, UnitSignoff, InductionForm are NSQ-only. RPL never creates these rows (RPL templates never use evidence_review / form_submission for unit sign-off).
  • Evidence Vault (GeneralEvidence / SelfAssessment / ThirdPartyReportEvidence) is RPL-only — distinct from NSQ per-unit evidence.
  • ApplicationUnit is shared — RPL still selects sector/trade/units at start; assessment is holistic via interview panel, not per-unit NOS checklist.
  • SignoffRole.UNIT_ASSESSOR (not ASSESSOR) avoids colliding with identity-level AssessorProfile.
Do Don’t
Keep filter/join fields as columns (status, centreId, currentStage) Bury join keys inside JSON
schemaVersion + Zod/TS validate per type+version Assume one eternal JSON shape
Form content may be JSON (incl. self-assessment competencies/reflection/declaration as opaque JSON until product locks shape) Form workflow state (status, who signed, when) stays columns
Centre.address structured JSON from organisation onboarding Invent a parallel trade on retained requests — use AssessorSectorExperience / sectors