Skip to Content
Data Model

Data Model Reference

Complete reference for all database tables, fields, and relationships.

Entity Relationship

┌──────────────┐ │ Organization │ └──────┬───────┘ │ 1:many ├──────────────────┐ │ │ ┌──────▼───────┐ ┌──────▼──────┐ │ API Key │ │ Conversation │ └──────────────┘ └──────┬──────┘ │ 1:many ┌─────┴─────┐ │ │ ┌──────▼──┐ ┌────▼────────┐ │ Message │ │ Chunk │ └─────────┘ └─────────────┘

Organizations

The top-level tenant. All data is scoped to an organization.

FieldTypeConstraintsDescription
idTEXTPRIMARY KEYPrefixed nanoid: org_ + 21 chars
nameTEXTNOT NULLDisplay name for the organization
created_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp
updated_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp

Example:

{ "id": "org_V1StGXR8_Z5jdHi6B-myT", "name": "Acme Corp", "created_at": "2026-03-23T10:00:00Z", "updated_at": "2026-03-23T10:00:00Z" }

API Keys

Authentication credentials tied to an organization.

FieldTypeConstraintsDescription
idTEXTPRIMARY KEYPrefixed nanoid: key_ + 21 chars
organization_idTEXTNOT NULL, FOREIGN KEY → organizations(id)The owning organization
key_hashTEXTNOT NULL, UNIQUESHA-256 hash of the raw API key (64 hex chars)
key_prefixTEXTNOT NULLFirst 20 characters of the raw key for identification
nameTEXTNOT NULLHuman-readable name for the key
expires_atTEXTNULLABLEISO 8601 expiration timestamp
revoked_atTEXTNULLABLEISO 8601 revocation timestamp
last_used_atTEXTNULLABLEISO 8601 timestamp of last use
created_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp

Raw key format: engram_sk_live_ + 32 random characters (47 chars total)

The raw key is shown once at creation and never stored. Only the SHA-256 hash is persisted.

Indexes:

  • idx_api_keys_hash on key_hash — fast lookup during authentication
  • idx_api_keys_org on organization_id — list keys per org

Example:

{ "id": "key_K2RtHYS9_A6keFj7C-nzU", "organization_id": "org_V1StGXR8_Z5jdHi6B-myT", "key_hash": "a1b2c3d4e5f6...64 hex chars", "key_prefix": "engram_sk_live_aBcD", "name": "Production key", "expires_at": null, "revoked_at": null, "last_used_at": "2026-03-23T15:42:00Z", "created_at": "2026-03-23T10:00:00Z" }

Conversations

A container for an ordered sequence of messages.

FieldTypeConstraintsDescription
idTEXTPRIMARY KEYPrefixed nanoid: conv_ + 21 chars
organization_idTEXTNOT NULL, FOREIGN KEY → organizations(id)The owning organization
titleTEXTNULLABLEHuman-readable title
agent_idTEXTNULLABLEIdentifier for the agent that created this conversation
tagsTEXTNOT NULL, DEFAULT ’[]‘JSON array of string tags
metadataTEXTNOT NULL, DEFAULT ''JSON object of arbitrary key-value pairs
message_countINTEGERNOT NULL, DEFAULT 0Number of messages in this conversation
created_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp
updated_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp

Indexes:

  • idx_conversations_org on organization_id — list conversations per org
  • idx_conversations_agent on agent_id — filter by agent
  • idx_conversations_updated on updated_at — sort by recency

Notes:

  • tags is stored as a JSON string (e.g., '["support","billing"]') and parsed to string[] in application code
  • metadata is stored as a JSON string and parsed to Record<string, unknown> in application code
  • message_count is incremented atomically on each append_messages call

Example:

{ "id": "conv_V1StGXR8_Z5jdHi6B-myT", "organization_id": "org_V1StGXR8_Z5jdHi6B-myT", "title": "Debugging the auth flow", "agent_id": "support-bot-v2", "tags": ["engineering", "auth"], "metadata": { "sprint": "2026-Q1-W12" }, "message_count": 24, "created_at": "2026-03-23T10:00:00Z", "updated_at": "2026-03-23T10:45:00Z" }

Messages

Individual messages within a conversation, stored verbatim.

FieldTypeConstraintsDescription
idTEXTPRIMARY KEYPrefixed nanoid: msg_ + 21 chars
conversation_idTEXTNOT NULL, FOREIGN KEY → conversations(id) ON DELETE CASCADEParent conversation
organization_idTEXTNOT NULLDenormalized org ID for tenant isolation
roleTEXTNOT NULL, CHECK (role IN (‘user’,‘assistant’,‘system’,‘tool’))Message author role
contentTEXTNOT NULLVerbatim message content
tool_call_idTEXTNULLABLEID of the tool call this message responds to
tool_nameTEXTNULLABLEName of the tool that was invoked
sequenceINTEGERNOT NULLPosition within the conversation (1, 2, 3, …)
metadataTEXTNOT NULL, DEFAULT ''JSON object of arbitrary key-value pairs
created_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp

Indexes:

  • idx_messages_conv_seq on (conversation_id, sequence) — ordered retrieval
  • idx_messages_org on organization_id — tenant-scoped queries

Key design decisions:

  • organization_id is denormalized — it’s redundant with the conversation’s org, but avoids JOINs for tenant-scoped queries
  • sequence is an integer, not a timestamp — deterministic ordering regardless of insertion time
  • content is stored as-is — no truncation, no summarization, no HTML encoding
  • Cascade delete: when a conversation is deleted, all its messages are automatically removed

Example:

{ "id": "msg_K2RtHYS9_A6keFj7C-nzU", "conversation_id": "conv_V1StGXR8_Z5jdHi6B-myT", "organization_id": "org_V1StGXR8_Z5jdHi6B-myT", "role": "tool", "content": "{\"invoices\": [{\"id\": \"inv_123\", \"amount\": 99.00}]}", "tool_call_id": "call_abc", "tool_name": "lookup_billing", "sequence": 3, "metadata": {}, "created_at": "2026-03-23T10:00:03Z" }

Conversation Chunks

Sliding-window text fragments of messages, used for embedding and vector search.

FieldTypeConstraintsDescription
idTEXTPRIMARY KEYPrefixed nanoid: chk_ + 21 chars
conversation_idTEXTNOT NULL, FOREIGN KEY → conversations(id) ON DELETE CASCADEParent conversation
organization_idTEXTNOT NULLDenormalized org ID for tenant isolation
chunk_textTEXTNOT NULLFormatted text of the message window
start_sequenceINTEGERNOT NULLFirst message sequence in this chunk
end_sequenceINTEGERNOT NULLLast message sequence in this chunk
vectorize_idTEXTNOT NULLUnique ID for the vector in Vectorize
created_atTEXTNOT NULL, DEFAULT CURRENT_TIMESTAMPISO 8601 timestamp

Indexes:

  • idx_chunks_conv on conversation_id — list chunks per conversation
  • idx_chunks_org on organization_id — tenant-scoped queries
  • idx_chunks_vectorize on vectorize_id — lookup by vector ID after search

Chunk text format:

[user]: Can you check the logs? [assistant]: Sure, looking now. [tool]: {"errors": [{"level": "ERROR"}]} [assistant]: I see an error in the logs. [user]: When did it start?

Chunking algorithm:

  • Window size: 5 messages
  • Stride: 3 messages
  • Overlap: 2 messages between consecutive chunks
  • Messages sorted by sequence before chunking

Example:

{ "id": "chk_M3SuIZT0_B7lfGk8D-ozV", "conversation_id": "conv_V1StGXR8_Z5jdHi6B-myT", "organization_id": "org_V1StGXR8_Z5jdHi6B-myT", "chunk_text": "[user]: Can you check...\n[assistant]: Sure...", "start_sequence": 1, "end_sequence": 5, "vectorize_id": "chk_M3SuIZT0_B7lfGk8D-ozV", "created_at": "2026-03-23T10:00:00Z" }

ID Format

All IDs use prefixed nanoids for human readability and type safety.

PrefixEntityExample
org_Organizationorg_V1StGXR8_Z5jdHi6B-myT
conv_Conversationconv_K2RtHYS9_A6keFj7C-nzU
msg_Messagemsg_M3SuIZT0_B7lfGk8D-ozV
key_API Keykey_N4TvJAU1_C8mgHl9E-paW
chk_Chunkchk_O5UwKBV2_D9nhIm0F-qbX

IDs are generated using nanoid with a default size of 21 characters (plus the prefix). They are URL-safe and contain characters from A-Za-z0-9_-.


Vector Metadata

Each vector stored in Vectorize includes metadata for filtering:

FieldTypeDescription
organization_idstringOrg scope for tenant isolation
conversation_idstringSource conversation
start_sequencenumberFirst message sequence in the chunk
end_sequencenumberLast message sequence in the chunk
Last updated on