WordloopWordloop
Reference

Database Schema

Postgres schema owned by wordloop-core, including tables, relationships, and migration workflow.

Database Schema

The Postgres database is owned exclusively by wordloop-core. The canonical schema is the target-state SQL file at services/wordloop-core/db/schema.sql.

Important

Wordloop Core does not use timestamped up/down migration files. To change the schema, edit services/wordloop-core/db/schema.sql, review the generated pg-schema-diff plan with ./dev db dry-run, then apply it with ./dev db migrate. Do not manually alter local, staging, or production databases.

Schema Workflow

wordloop-core uses a declarative schema workflow:

  1. services/wordloop-core/db/schema.sql describes the desired final database state.
  2. services/wordloop-core/cmd/migrate connects using DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, and DB_SSLMODE.
  3. The migrator ensures the vector extension exists, builds a temporary database, applies the target schema there, and asks pg-schema-diff to compare the live database to the target schema.
  4. ./dev db dry-run prints the planned DDL and hazards without applying changes.
  5. ./dev db migrate applies the generated plan to the configured database.
  6. ./dev db dump applies the target schema locally, then writes a reference-only pg_dump snapshot to .dev/schema.sql.

The Docker Compose core profile builds the same migrator binary and runs the wordloop-core-migrate container before starting the API container.

ER Diagram

Rendering architecture map...

Enums

EnumValues
public.task_source_enumuser, system
public.meeting_source_enumrecording, upload, text, anecdotal

Tables

users

Primary user account linked to Clerk and optionally to a people row for the user's own person record.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
usernameTEXTRequired
emailTEXT UNIQUERequired
clerk_idTEXT UNIQUEExternal identity
person_idUUID FK -> people.idNullable, ON DELETE SET NULL
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Indexes: idx_users_person_id on person_id.

people

Contacts and meeting participants scoped to a user.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
display_nameTEXTRequired
full_nameTEXTOptional
titleTEXTOptional
roleTEXTOptional
emailTEXTOptional
companyTEXTOptional
tagsJSONBOptional
voice_model_statusTEXTDefaults to untrained; check: untrained, training, ready
voice_confidenceDECIMALOptional
voice_vectorvector(192)Optional ECAPA-TDNN speaker embedding. Locked to 192 dimensions in lockstep with ML's embedding model — see ADR 0008.
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Constraints and indexes: unique (user_id, display_name), idx_people_voice_vector_hnsw on voice_vector using HNSW cosine ops.

person_summaries

Summaries of people authored by users.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
person_idUUID FK -> people.idRequired, ON DELETE CASCADE
author_idUUID FK -> users.idRequired
summaryTEXTRequired
contextJSONBOptional supporting context
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

tags

User-owned tag names.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
nameTEXTRequired
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

meetings

Recorded, uploaded, text, or anecdotal conversations.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
titleTEXTRequired
source_typepublic.meeting_source_enumRequired
start_timeTIMESTAMPTZRequired
end_timeTIMESTAMPTZOptional
summaryTEXTOptional AI-generated summary
key_pointsJSONBOptional
headlineTEXTOptional
deleted_atTIMESTAMPTZSoft-delete marker
created_atTIMESTAMPTZDefaults to now()

meeting_audio_files

Audio object attached to a meeting.

ColumnTypeNotes
meeting_idUUID PK/FK -> meetings.idRequired, ON DELETE CASCADE
storage_pathTEXTRequired GCS/emulator storage path
created_atTIMESTAMPTZDefaults to now()

meeting_attendees

Join table between meetings and people.

ColumnTypeNotes
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
person_idUUID FK -> people.idRequired, ON DELETE CASCADE

Constraints and indexes: primary key (meeting_id, person_id), idx_meeting_attendees_meeting_id, idx_meeting_attendees_person_id.

transcriptions

Transcription job state for a meeting.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
statusTEXTRequired; check: pending, transcribing, synthesizing, completed, failed
status_messageTEXTOptional error or status detail
is_degradedBOOLEANRequired, defaults to false
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

transcription_status_history

Audit log of transcription status changes.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
transcription_idUUID FK -> transcriptions.idRequired, ON DELETE CASCADE
statusTEXTRequired
status_messageTEXTOptional
created_atTIMESTAMPTZDefaults to now()

transcript_segments

Timestamped chunks of transcript text.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
transcription_idUUID FK -> transcriptions.idNullable, ON DELETE CASCADE
person_idUUID FK -> people.idNullable, ON DELETE SET NULL
speaker_labelTEXTTemporary label before identification
textTEXTRequired
start_msDECIMALRequired start offset in milliseconds
end_msDECIMALRequired, defaults to 0
confidenceDECIMALRequired, defaults to 0
is_finalBOOLEANRequired, defaults to true
is_highlightedBOOLEANRequired, defaults to false
feature_vectorvector(192)Optional segment embedding — ECAPA-TDNN, matches people.voice_vector. See ADR 0008.

Indexes: idx_transcript_time on (transcription_id, start_ms), idx_transcript_segments_feature_vector_hnsw on feature_vector using HNSW cosine ops, idx_transcript_segments_person_id on person_id.

segment_manifest

One row per GCS storage segment — an aggregated object covering a contiguous, inclusive range of 100ms audio frame sequences for a live recording. The authoritative highest_contiguous_sequence / missing-range computation for a recording is derived from this table.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
audio_versionINTEGERRequired, defaults to 1. Partitions takes — re-recording a meeting starts a new version, so a new take's segments never collide with the previous take's manifest rows.
start_sequenceBIGINTRequired
end_sequenceBIGINTRequired
started_at_msBIGINTRequired
duration_msBIGINTRequired
object_nameTEXTRequired GCS object name
object_generationBIGINTOptional
byte_countBIGINTRequired
crc32cTEXTRequired
sha256TEXTOptional; NULL for live segments, populated for gap-recovery segments
created_atTIMESTAMPTZDefaults to now()

Constraints and indexes: unique (meeting_id, audio_version, start_sequence, end_sequence), idx_segment_manifest_meeting_version_seq on (meeting_id, audio_version, start_sequence).

meeting_speaker_labels

User-owned speaker_label → person assignments for a meeting. Re-applied to transcript_segments whenever segments are appended (live) or replaced (final batch rebuild), so assignments survive transcript rebuilds.

ColumnTypeNotes
meeting_idUUID PK/FK -> meetings.idRequired, ON DELETE CASCADE
speaker_labelTEXT PKRequired
person_idUUID FK -> people.idRequired, ON DELETE CASCADE
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Constraints and indexes: primary key (meeting_id, speaker_label), idx_meeting_speaker_labels_person_id on person_id.

recording_event_history

Append-only diagnostics trail for a meeting's live recording: every lifecycle transition, health event, reconnect/resume, duration warning, and stop (with reason). Served newest-first by GET /meetings/{id}/recording/events.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
seqBIGSERIALAppend order; created_at alone is not unique inside a transaction
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
event_typeTEXTRequired
payloadJSONBOptional
created_atTIMESTAMPTZDefaults to clock_timestamp()

Indexes: idx_recording_event_history_meeting on (meeting_id, seq DESC).

topics

Topic clusters for a meeting.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
nameTEXTRequired
summaryTEXTRequired
is_finalBOOLEANRequired, defaults to false
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Indexes: idx_topics_meeting_id on meeting_id.

topic_segments

Segments associated with a topic.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
topic_idUUID FK -> topics.idRequired, ON DELETE CASCADE
segment_idUUIDRequired segment identifier
created_atTIMESTAMPTZDefaults to now()

Indexes: idx_topic_segments_topic_id on topic_id.

talking_points

Meeting talking points, optionally linked to a topic.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
meeting_idUUID FK -> meetings.idRequired, ON DELETE CASCADE
topic_idUUID FK -> topics.idNullable, ON DELETE SET NULL
contentTEXTRequired
is_finalBOOLEANRequired, defaults to false
created_atTIMESTAMPTZDefaults to now()
deleted_atTIMESTAMPTZSoft-delete marker

talking_point_segments

Join table between talking points and segment identifiers.

ColumnTypeNotes
talking_point_idUUID FK -> talking_points.idRequired, ON DELETE CASCADE
segment_idUUIDRequired segment identifier

Constraints: primary key (talking_point_id, segment_id).

tasks

Actionable items owned by users and optionally sourced from meetings.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
contentTEXTRequired
statusTEXTRequired, defaults to pending; check: pending, completed
sourcepublic.task_source_enumRequired, defaults to system
due_dateDATEOptional
assigned_toUUID FK -> people.idNullable, ON DELETE SET NULL
meeting_idUUID FK -> meetings.idNullable, ON DELETE SET NULL
parent_task_idUUID FK -> tasks.idNullable, ON DELETE CASCADE
deleted_atTIMESTAMPTZSoft-delete marker
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Indexes: idx_tasks_status_due on (user_id, status, due_date).

sub_tasks

Legacy subtask rows associated with tasks.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
task_idUUID FK -> tasks.idRequired, ON DELETE CASCADE
contentTEXTRequired
statusTEXTRequired, defaults to pending; check: pending, completed
assigned_toUUID FK -> people.idNullable, ON DELETE SET NULL
due_dateDATEOptional
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Indexes: idx_sub_tasks_task_id on task_id.

notes

Free-form notes attached to people or meetings.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
contentTEXTRequired
subject_typeTEXTOptional; check: PERSON, MEETING
subject_idUUIDOptional polymorphic subject identifier
tagsJSONBOptional
created_atTIMESTAMPTZDefaults to now()
updated_atTIMESTAMPTZDefaults to now()

Indexes: idx_notes_subject on (subject_type, subject_id).

ai_threads

Contextual AI conversation containers.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
user_idUUID FK -> users.idRequired, ON DELETE CASCADE
context_typeTEXTOptional; check: PERSON, MEETING
context_idUUIDOptional polymorphic context identifier
deleted_atTIMESTAMPTZSoft-delete marker
created_atTIMESTAMPTZDefaults to now()

chat_messages

Individual messages within an AI thread.

ColumnTypeNotes
idUUID PKDefaults to gen_random_uuid()
thread_idUUID FK -> ai_threads.idRequired, ON DELETE CASCADE
roleTEXTRequired; check: user, assistant, system, tool
contentTEXTRequired
tool_callsJSONBOptional
deleted_atTIMESTAMPTZSoft-delete marker
created_atTIMESTAMPTZDefaults to now()

Indexes: idx_chat_messages_thread_id on thread_id.

On this page