DocsArchitectureData Model

Data Model

Database schema and entity relationships for Reqcore. Covers jobs, candidates, applications, documents, and multi-tenant isolation.

Data Model

Reqcore uses PostgreSQL 16 with Drizzle ORM for type-safe database access. All domain tables are defined in server/database/schema/app.ts.

Entity Relationship

organization (Better Auth)
├── job (draft → open → closed → archived)
│   ├── jobQuestion (custom application form questions)
│   └── application (new → screening → interview → offer → hired/rejected)
│       ├── questionResponse (answers to custom questions)
│       └── candidate (deduplicated by email per org)
│           └── document (resume, cover_letter — stored in S3)
└── member (user ↔ organization with role)

Tables

job

The core hiring entity. Each job belongs to an organization.

ColumnTypeDescription
iduuidPrimary key
organizationIdstringFK → organization
titlestringJob title
descriptiontextFull description (Markdown)
statusenumdraft, open, closed, archived
locationstringOffice location
employmentTypestringFull-time, Part-time, etc.
slugstringURL slug for public page
salaryMin / salaryMaxintegerSalary range
salaryCurrencystringCurrency code (USD, EUR, etc.)
salaryUnitstringHOUR, DAY, WEEK, MONTH, YEAR
remoteStatusstringOn-site, Remote, Hybrid
validThroughtimestampApplication deadline
createdAt / updatedAttimestampTimestamps

candidate

A person who has applied or been added to the talent pool.

ColumnTypeDescription
iduuidPrimary key
organizationIdstringFK → organization
firstName / lastNamestringFull name
emailstringEmail (unique per org)
phonestringPhone number
notestextRecruiter notes
createdAt / updatedAttimestampTimestamps

Unique constraint: (organizationId, email) — prevents duplicate candidates within an org.

application

Links a candidate to a job with a pipeline status.

ColumnTypeDescription
iduuidPrimary key
organizationIdstringFK → organization
jobIduuidFK → job
candidateIduuidFK → candidate
statusenumnew, screening, interview, offer, hired, rejected
notestextApplication-specific notes
scoreintegerRecruiter-assigned score
createdAt / updatedAttimestampTimestamps

Unique constraint: (organizationId, candidateId, jobId) — one application per candidate per job.

document

A file (resume, cover letter, work sample) stored in S3.

ColumnTypeDescription
iduuidPrimary key
organizationIdstringFK → organization
candidateIduuidFK → candidate
fileNamestringOriginal filename (sanitized)
mimeTypestringFile MIME type
fileSizeintegerFile size in bytes
storageKeystringS3 object key (never exposed to clients)
documentTypeenumresume, cover_letter, other
createdAttimestampUpload timestamp

jobQuestion

Custom application form questions per job.

ColumnTypeDescription
iduuidPrimary key
jobIduuidFK → job
questionTextstringQuestion text
questionTypeenumshort_text, long_text, single_select, multi_select, number, date, url, email, file_upload
requiredbooleanWhether answer is required
optionsjsonOptions for select types
sortOrderintegerDisplay order

questionResponse

Answers to custom questions per application.

ColumnTypeDescription
iduuidPrimary key
applicationIduuidFK → application
questionIduuidFK → jobQuestion
responseValuetextThe answer

Multi-Tenant Isolation

Every domain table includes an organizationId column. All database queries must include an organizationId filter derived from the authenticated session — never from user input.

Example query pattern:

const jobs = await db
  .select()
  .from(job)
  .where(eq(job.organizationId, orgId))

Migrations

Database migrations are managed by Drizzle Kit:

# Generate a migration after schema changes
npm run db:generate

# Apply migrations manually
npm run db:migrate

Migrations also run automatically on server startup via the server/plugins/migrations.ts plugin, using a PostgreSQL advisory lock for safety in multi-instance deployments.

Next Steps