All schema, migrations, and the generated client live in packages/database. The database is the canonical source of truth for the platform — every app reads its types from @repo/database and never re-declares them.
- Schema source:
packages/database/prisma/schema.prisma.
- Migrations:
packages/database/prisma/migrations/<YYYYMMDDHHmmss>_<snake_case>/.
- Generated client is the Prisma client — apps import it via
@repo/database.
- New models follow the existing style:
id String @id @default(cuid()), createdAt DateTime @default(now()), updatedAt DateTime @updatedAt, and indexes on every column commonly used for filtering or scoping (especially organizationId).
- Soft / “legacy” columns are kept around with a comment when a structured replacement supersedes them; the rename is sequenced over multiple migrations.
Organization — a tenant. Owns candidate profiles (when staffed), job postings, applications, and audit logs.
WorkOsUser — local mirror of WorkOS user metadata, populated by user.created / user.updated webhooks. Used for relational integrity when local rows need a foreign key to a user.
CandidateProfile — the canonical candidate record. Owns resume, skills, work prefs, links, and discoverability state.
CandidateFileObject — S3-backed file references for profile picture and resume.
ResumeConversion — async resume → markdown extraction state and result.
PublicResumeLink — token-gated shareable URL for a candidate’s resume.
CandidateInvitation — token-gated invite for a candidate to claim or onboard.
JobPosting — an open role at an organization.
JobPostingPublicLink — token-gated public URL to a job posting.
JobPostingRequest — the company intake form (a request that produces a JobPosting once submitted).
Application — a candidate’s application to a posting; unique per (jobPostingId, candidateProfileId).
ApplicationStageEvent — every advance/reject/withdraw event, with actor and timestamp.
CandidateNote — internal / application-scoped notes left on candidates.
NormalizedSkill — canonical skill registry with parent/child grouping and approval state.
NormalizedSkillSource — link table between a CandidateProfile’s raw skill string and a NormalizedSkill.
AuditLog — append-only record of privileged actions, indexed by organizationId, actorSub, action, targetType, and createdAt.
- Edit
packages/database/prisma/schema.prisma.
- Run
pnpm run db:migrate:dev from the repo root (or npx prisma migrate dev from packages/database).
- Commit the generated migration in
packages/database/prisma/migrations/.
- Regenerate clients via
pnpm run db:generate.
- If the change renames or removes a field, update every consuming app in the same PR.
For the most permission-sensitive models, the matrix below documents who can read and who can write each field. Until field-level extraction is automated, this is hand-maintained and should be updated when a service changes its field filtering.
| Field | Read | Write |
|---|
firstName, lastName, headline, bio, location* | owner member, company actors w/ app context, teros-ops* | owner member, teros-ops* |
skills, workPreferences, portfolioLinks, yearsOfExperience | same as above | same as above |
linkedinUrl, githubUrl | same as above | same as above |
phoneNumber, phoneCountryCode | owner member, teros-ops* | owner member, teros-ops* |
profileImage*, resume* | owner member, company actors w/ app context, teros-ops* | owner member, teros-ops* |
isDiscoverable | owner member, teros-ops* | owner member, teros-ops* |
approvalStatus, approvedAt, approvedByWorkosUserSub | teros-ops* | teros-ops* |
salaryExpectation*, currentSalary | owner member, teros-ops* | owner member, teros-ops* |
| Field | Read | Write |
|---|
coverNote | candidate (own), company actors of posting org, teros-ops* | candidate at submission only |
status | candidate (own), company actors, teros-ops* | company-admin, teros-ops*; company-member limited |
offerStartDate, offeredSalary | candidate (own), company-admin, teros-ops* | company-admin, teros-ops* |
withdrawnAt | candidate (own), company actors, teros-ops* | candidate (own withdraw), teros-ops* |
terminatedAt | company-admin, teros-ops* | company-admin, teros-ops* |
| Field | Read | Write |
|---|
title, description, location*, employmentType, isRemote, currency | public (via public link or list), all roles | company-admin, teros-ops* |
minCompensation, maxCompensation | public listing, all internal roles | company-admin, teros-ops* |
status, openedAt, closedAt | public listing, all internal roles | company-admin, teros-ops* |
createdBySub, updatedBySub | company-admin, teros-ops* | system-managed |
| Field | Read | Write |
|---|
action, actorSub, targetType, targetId, createdAt, metadata | company-admin (scoped to own organizationId), teros-ops* (cross-org) | system |
organizationId, actorOrgId | same as above | system |
| Field | Read | Write |
|---|
content (visibility = INTERNAL) | teros-ops* only | teros-ops* |
content (visibility = APPLICATION) | company actors of the posting org, teros-ops* | company actors of the posting org, teros-ops* |
authorSub | same as content for that visibility | system-managed |
| Field | Read | Write |
|---|
name, slug, description | members of org, teros-ops* | company-admin (own org), teros-ops* |
workosOrganizationId, ownerWorkosUserId | teros-ops* | system-managed (WorkOS sync) |
- API — how services enforce these access decisions.
- Migrations — the loop for changing the schema safely.
- Permissions Matrix — endpoint-level view of the same model.