Data model
The Convex tables that back documents, their extracted content, and conversations — plus the indexes that power retrieval.
Tables
5 core
Relations
5 foreign keys
Embeddings
1536-dim
Indexes
Vector + text
Schema at a glance
Five tables back the product. A documents row owns its extracted documentPages and documentChunks, plus every conversations thread started against it; each thread owns its messages. Foreign keys are Convex document IDs (v.id("table")), and every content row also carries an ownerTokenIdentifier that authorization filters on for every read.
One row per uploaded PDF; tracks its lifecycle and artifacts.
_ididPKownerTokenIdentifierstringOWNIDXtitlestringoriginalFilenamestringsha256stringfileStorageId?id→ _storageFKstatusunionIDXpageCount?numberdocumentSummarystringprocessingError?stringlastProcessedAt?numberembeddedChunkCount?number
OCR text and a summary for a single page of a document.
_ididPKdocumentIdid→ documentsFKIDXownerTokenIdentifierstringOWNIDXpageNumbernumberIDXextractedTextstringsummarystringembedding?float64[1536]VEC
The retrieval unit — overlapping text windows with page spans.
_ididPKdocumentIdid→ documentsFKIDXownerTokenIdentifierstringOWNIDXchunkIndexnumberIDXstartPageNumbernumberendPageNumbernumbertextstringFTStokenCountnumberpageSpansobject[]embeddingfloat64[1536]VEC
A chat thread scoped to a single document.
_ididPKdocumentIdid→ documentsFKIDXownerTokenIdentifierstringOWNIDXtitlestring
A single turn in a conversation, with validated citations.
_ididPKconversationIdid→ conversationsFKIDXroleunioncontentstringstatus?unioncitations?object[]→ documentChunks
documents1 : NdocumentPageseach page of the PDFdocuments1 : NdocumentChunksretrieval windowsdocuments1 : Nconversationschats about the docconversations1 : Nmessagesturns in a threaddocumentChunks0 : Nmessagescited by (citations[].chunkId)
Trimmed to the columns that carry weight — see “Columns trimmed from the design” below for what was removed and why.
Entity relationships
The same model as an entity-relationship diagram. Solid edges are hard foreign keys; the dashed edge is a soft reference — a message's citations point back at the documentChunks they quote.
Indexes & citation fields
Indexes that power retrieval
documentChunks carries a vector index by_embedding (1536-dim, filtered by documentId) and a full-text search_text index. Hybrid retrieval queries both and fuses the results; documentPages keeps its own vector index for page-level search.Two composite fields do the heavy lifting for grounded answers:
| Field | Type | Description |
|---|---|---|
| documentChunks.pageSpans | object[] | { pageNumber, startOffset, endOffset } — maps any position in a chunk back to a page, so a quote resolves to the page it came from. |
| messages.citations | object[]? | Validated citations: pageNumber, snippet, chunkId (→ documentChunks), verbatim quote, and its character offsets. |
Columns trimmed from the design
The live schema stores extra provenance the design above leaves out. These are safe candidates to drop — they duplicate information already available from status, _creationTime, or environment config:
documents: per-stage timestamps (uploadCompletedAt,processingStartedAt,ocrCompletedAt,embeddingsCompletedAt) collapse into_creationTime+lastProcessedAt+status.documents: model / provider strings (summaryModel,embeddingModel,ocrModel,ocrMethod,ocrProvider) and vendor handles (mistralFileId,ocrResultStorageId,storageContentType,processingAttemptCount) are config- or log-level detail, not query inputs.documentPages/documentChunks:extractionMethodand per-rowembeddingModel/embeddingTokenCountare constant or derivable;ownerDocumentKeyduplicatesownerTokenIdentifier+documentId.conversations/messages: explicitcreatedAtduplicates Convex's built-in_creationTime.
Documentation only
convex/schema.ts — removing a column also means updating every mutation and query that writes or reads it.