Skip to Content
Data ArchitectureTransactional Schema

Transactional Schema

Schema Ownership

OpenTier uses a single PostgreSQL database shared between both services. All schema migrations are centralized in a single location:

Migration ToolLocationTables
sqlx-cliserver/db/migrations/users, sessions, accounts, verification_tokens, password_reset_tokens, conversations, messages, documents, document_chunks, ingestion_jobs, chat_messages, user_memories, knowledge_submissions

Migrations are applied automatically by the API container during its startup sequence before the server boots. The API container uses sqlx migrate run underneath to apply standard {timestamp}_{name}.up.sql / .down.sql files directly.

Entity Relationship Diagram

Table Details

users

ColumnTypeNotes
idUUIDgen_random_uuid()
emailVARCHAR(255)Unique, indexed
email_verifiedBOOLEANDefault false; OAuth users set true immediately
password_hashVARCHAR(255)bcrypt; NULL for OAuth-only accounts
roleuser_role ENUMuser, contributor, or admin
deleted_atTIMESTAMPTZSoft-delete; NULL = active

Indexes: idx_users_email, idx_users_username, idx_users_role, idx_users_created_at, idx_users_deleted_at (partial, WHERE deleted_at IS NULL)
Trigger: update_updated_at_column() updates updated_at on every UPDATE

sessions

ColumnTypeNotes
session_tokenVARCHAR(255)64-char random alphanumeric; unique
expires_atTIMESTAMPTZNOW() + 7 days
roleuser_role ENUMDuplicated from users.role at session creation time
ip_addressINETClient IP (IPv4 or IPv6)

Key design note: role in sessions is a snapshot taken at login. Role changes do not propagate to active sessions.

accounts (OAuth)

Enforces UNIQUE(provider, provider_account_id) — one account per provider per external identity. One user can link multiple providers.

conversations

ColumnTypeNotes
idUUIDNo DEFAULT — caller provides UUID
user_idVARCHAR(255)Not a foreign key to users.id — loose coupling

Critical: user_id in conversations is a VARCHAR(255), not a UUID FK referencing users. This allows the Python Intelligence service to create or reference conversations without strict FK integrity — enabling the shared-table architecture without Rust owning all writes.

messages vs chat_messages

Two tables store message content. This is the most architecturally significant split in the schema:

TableWritten byRead byContains
messagesRust APIRust APIMinimal message rows (role, content, metadata); no sources
chat_messagesPython IntelligenceBoth servicesFull AI messages with sources (JSONB), parent_id for branching

In practice the Rust API uses chat_messages (via get_conversation) and messages appears to be vestigial or used for audit logging. This creates a dual-table situation that must be managed carefully during schema changes.

knowledge_submissions

Stores contributor-submitted resources pending admin review.

ColumnTypeNotes
contributor_idUUID FKReferences users.id
statusVARCHAR(20)pending, approved, rejected
metadataJSONBFree-form submission metadata
reviewed_byUUID FKAdmin reviewer (users.id)

Indexes: idx_knowledge_submissions_status, idx_knowledge_submissions_contributor, idx_knowledge_submissions_created_at.

Enums

CREATE TYPE user_role AS ENUM ('user', 'contributor', 'admin'); CREATE TYPE message_role AS ENUM ('user', 'assistant', 'system');

user_role is used in both users.role and sessions.role. In chat_messages, role is VARCHAR(50) (written by Python) — not the PostgreSQL enum type.

Last updated on