Skip to content

Column-by-Column Schema Reference

Every column for every table, derived from the migration files (migrations/*.js) and cross-checked against the DAL reader functions in lib/dal.postgres.js. The migration files are the schema-of-record; this doc is the working reference — keep both in sync per the Schema-change protocol in CLAUDE.md.

When writing SQL in specs or tests, check here first. The three most common hallucination traps:

  • users has status ('active' / 'inactive'), not is_active
  • company_calendar has no status column — only id, date, name, type
  • time_entries date column is date, not entry_date

All tables include tenant_id uuid NOT NULL as part of their composite primary key (tenant_id, id). It is omitted from the per-column tables below to reduce noise; assume it is always present and always the first primary key component.


time_entries

ColumnTypeDefaultNullableNotes
idintegerNOPK component (with tenant_id)
datedateNOEntry date. Column is named date, not entry_date.
user_idintegerNOusers.id
project_idintegerNOprojects.id
task_idintegerYEStasks.id. Nullable — some overhead entries have no task.
hoursnumeric(5,2)NO
notestextYES

The person column from the legacy Sheets schema was not migrated to Postgres. The DAL reader synthesises person: '' on every row for Sheets-compatibility — it is not a real column.


projects

ColumnTypeDefaultNullableNotes
idintegerNOPK component
nametextNO
client_idintegerYESclients.id
typetextNO'Retainer' | 'retainer_period' | 'agile' | 'sprint' | 'T&M' | 'Fixed Price' | 'Pro Bono' | 'Internal'
is_billablebooleanfalseNO
contract_valuenumeric(12,2)YES
hours_budgetnumeric(8,2)YESEmpty on Retainer/Agile parents — lives on children.
billing_ratenumeric(10,2)YES
alert_thresholdinteger80NOPercentage.
statustext'active'NO'active' | 'completed' | 'cancelled'
pm_user_idintegerYESusers.id
project_categorytext'client'NO'client' | 'internal' | 'pto' | 'sick' | 'leave'
notestextYES
deadlinedateYES
parent_project_idintegerYESprojects.id. Set on period/sprint children; null on parents.
period_numberintegerYESNull on parents.
period_statustextYES'planned' | 'active'. Null on parents.
period_startdateYESNull on parents.
period_enddateYESNull on parents.
renewal_cadencetextYES'monthly' | 'quarterly' | 'biannual' | 'annual'. Null on non-retainer parents.
start_datedateCURRENT_DATENOAdded in migration 0020. Backfilled from first time entry date on existing rows.

users

ColumnTypeDefaultNullableNotes
idintegerNOPK component
slack_user_idtextYESNullable since migration 0018 — contractors/vendors may have no Slack presence.
nametextNO
roletext'user'NO'owner' | 'manager' | 'user' | 'admin' (migration 0023). admin has owner-level access but is excluded from team-delivery statistics (see lib/roles.js); it is never normalised to 'owner'. Legacy Sheets value 'pm' is normalised to 'manager' by the DAL reader.
statustext'active'NO'active' | 'inactive'. Not a boolean — never use is_active.
discipline_idintegerYESdisciplines.id
employment_typetext'fte'NO'fte' | 'contractor' | 'vendor'
hours_per_weeknumeric(5,2)40NOFTE defaults 40. Contractors require explicit value.

clients

ColumnTypeDefaultNullableNotes
idintegerNOPK component
nametextNO
statustext'active'NO'active' | 'inactive'
notestextYES

tasks

ColumnTypeDefaultNullableNotes
idintegerNOPK component
nametextNO
is_billablebooleanfalseNO
statustext'active'NO'active' | 'archived'. Added in migration 0019.

project_tasks

ColumnTypeDefaultNullableNotes
project_idintegerNOPK component. → projects.id
task_idintegerNOPK component. → tasks.id
budget_hoursnumeric(8,2)YES
alert_thresholdinteger80NOPercentage.

This table has no id column. PK is (tenant_id, project_id, task_id). Presence of a row means the task is enabled for that project; there is no enabled flag.


budgets

ColumnTypeDefaultNullableNotes
idintegerNOPK component
user_idintegerNOusers.id
project_idintegerNOprojects.id
allocated_hoursnumeric(8,2)NO
statustext'active'NO'active' | 'cancelled' | 'transferred'
created_byintegerYESusers.id. Nullable — older envelopes predate this column.
created_attimestamptznow()NO
task_idintegerYEStasks.id. Optional work-category tag on the envelope.

transferred_from_allocation_id was dropped in migration 0021 — it was never populated by any tool. Do not reference it in SQL.


disciplines

ColumnTypeDefaultNullableNotes
idintegerNOPK component
nametextNO
statustext'active'NO'active' | 'inactive'

conversations

ColumnTypeDefaultNullableNotes
row_idbigserialNOSurrogate PK (single column). Not the conversation ID.
idtextNOConversation ID — not unique; multiple rows share the same id (one per exchange).
timestamptimestamptzNO
user_idtextNOSlack user ID (text). No FK to users.
user_nametextYES
messagetextNO
responsetextYES
summarytextYES
entitiesjsonb'[]'NOTyped entity references from this turn's tool calls. Added in migration 0022. Shape: array of entity objects consumed by the Phase 1 entity-resolver.

conversations uses row_id bigserial as its primary key, not (tenant_id, id) like other tables. tenant_id is still present and indexed for isolation.


pending_pto

ColumnTypeDefaultNullableNotes
idintegerNOPK component
user_idintegerNOusers.id
datedateNO
hoursnumeric(5,2)NO
statustext'pending'NO'pending' | 'approved' | 'rejected'
created_attimestamptznow()NO
approved_bytextYESSlack user ID of the approving manager (text, not internal user id).
resolved_attimestamptzYESSet when status moves to approved or rejected.
notestextYES

unresolved_items

ColumnTypeDefaultNullableNotes
idintegerNOPK component
typetextNOFreeform category string (e.g. 'time_entry').
descriptiontextNO
affected_project_idintegerYESprojects.id. May be null if item is user-scoped only.
affected_user_idintegerYESusers.id. May be null if item is project-scoped only.
created_attimestamptznow()NO
statustext'open'NO'open' | 'resolved' | 'dismissed'
resolved_attimestamptzYES
resolved_bytextYESSlack user ID of whoever resolved/dismissed the item (text).

company_calendar

ColumnTypeDefaultNullableNotes
idintegerNOPK component
datedateNO
nametextNOHoliday or event name.
typetext'holiday'NOCurrently always 'holiday'. CHECK kept loose to allow future values without a migration.

There is no status column on company_calendar. The full column list is: tenant_id, id, date, name, type.


rate_history

ColumnTypeDefaultNullableNotes
idintegerNOPK component
user_idintegerNOusers.id
hourly_costnumeric(10,2)NO
effective_fromdateNOEffective-date versioning. The rate in effect for a given entry date = MAX(effective_from) WHERE effective_from <= entry_date.

Append-only table — no UPDATE or DELETE in the DAL. New rates are always new rows.


liabilities

ColumnTypeDefaultNullableNotes
idintegerNOPK component
project_idintegerNOprojects.id
vendor_user_idintegerNOusers.id. Must reference a user with employment_type = 'vendor' (enforced at application layer, not schema).
descriptiontextNO
agreed_amountnumeric(12,2)NOFixed payment commitment. Accrues pro-rata to deadline.
statustext'active'NO'active' | 'cancelled'
created_bytextNOSlack user ID of the creating owner (text, not internal user id).
created_attimestamptznow()NO
agreed_hoursnumeric(10,2)0NOContracted effort in hours. Added in migration 0017. Accrues linearly from created_at to project deadline as implied hours in burn-down calculations. Rows predating the column have agreed_hours = 0 (inert).

VERA by talktalkmake