Database Architecture
November 1, 2025 · View on GitHub
Listopia uses PostgreSQL with UUID primary keys throughout. All model schema information is documented via the Annotate gem directly in model files—refer to those for detailed field definitions, indexes, and constraints.
Design Principles
- UUID Primary Keys - Every table uses UUID primary keys for security and distributed system readiness
- PostgreSQL Features - Leverages JSON/JSONB columns, advanced indexing, and constraints
- Solid Stack - Separate databases for cache, queue, and cable operations
- Data Integrity - Foreign key constraints and comprehensive model validations
Core Models
Detailed schema documentation is in each model file's == Schema Information block (via Annotate gem):
Authentication & Users
- User - User accounts with email verification and passwordless auth
- Session - Session tokens with expiration tracking
Lists & Collaboration
- List - Main document/list container with status tracking
- ListItem - Tasks, items within lists with priority/due dates
- ListCollaboration - Sharing permissions and collaborator management
- Invitation - Email-based collaboration invitations with tokens
AI & Chat
- Chat - Conversation containers with context snapshots
- Message - Messages with role tracking, tokens used, and tool calls
- Model - LLM model metadata (via Ruby LLM integration)
- ToolCall - AI tool invocations (for creating/updating lists)
Supporting Models
- Role - User roles for Rolify authorization system
- Tag - Tagging system for organizing lists and items
- NotificationSetting - User notification preferences
- Relationship - Polymorphic relationships (hierarchies, dependencies)
Key Relationships
User Account
User
├── has_many :lists (owner)
├── has_many :list_collaborations (collaborator)
├── has_many :collaborated_lists (through collaborations)
├── has_many :chats
├── has_many :messages (user-created)
├── has_one :notification_settings
└── has_and_belongs_to_many :roles (Rolify)
List Collaboration
List
├── belongs_to :owner (User)
├── has_many :list_items
├── has_many :list_collaborations
├── has_many :collaborators (through list_collaborations)
├── has_many :invitations
├── belongs_to :parent_list (optional, for hierarchies)
└── has_many :sub_lists
Chat System
Chat
├── belongs_to :user
├── belongs_to :model (optional, for LLM metadata)
└── has_many :messages
├── Message
│ ├── belongs_to :chat
│ ├── belongs_to :user (optional, nil for assistant)
│ └── has_many :tool_calls
Query Patterns
Finding Accessible Lists
# Lists a user can access (owned or collaborated)
user.lists + user.collaborated_lists
# Or use the scope
List.accessible_by(user)
Chat History with Context
# Load chat with full message history
chat = Chat.includes(:messages).find(id)
# Filter conversation messages (no tool messages)
chat.messages.conversation.order(:created_at)
# Get tool calls for debugging
chat.messages.where(role: :tool)
List Item Status Filtering
# Common queries
list.list_items.status_pending
list.list_items.status_completed
list.list_items.where.not(assigned_user_id: nil)
Database Configuration
Listopia uses multiple databases via Solid Stack:
production:
primary: &primary_production
database: listopia_production
cache:
<<: *primary_production
database: listopia_production_cache
migrations_paths: db/cache_migrate
queue:
<<: *primary_production
database: listopia_production_queue
migrations_paths: db/queue_migrate
cable:
<<: *primary_production
database: listopia_production_cable
migrations_paths: db/cable_migrate
Enums
Models use PostgreSQL enums (as Rails enums) for state tracking:
- List.status - draft, active, completed, archived
- ListItem.status - pending, in_progress, completed
- ListItem.priority - low, medium, high
- ListCollaboration.permission - view, comment, edit, admin
- Chat.status - active, archived, completed, workflow_planning, error
- Message.role - user, assistant, system, tool
- User.status - active, suspended, deactivated
Indexing Strategy
Strategic indexes are created for common query patterns:
# Foreign key indexes (automatic)
index :user_id
index :chat_id
index :list_id
# Composite indexes for common filters
index [:user_id, :status]
index [:chat_id, :role]
index [:chat_id, :created_at]
# Unique constraints where appropriate
index :email, unique: true
index :session_token, unique: true
See migrations in db/migrate/ for complete indexing strategy.
JSON Metadata Columns
Many models include metadata JSON field for flexible extension:
# Examples
list.metadata = { tags: ["work", "urgent"], custom_field: "value" }
message.context_snapshot = { list_id: "...", list_title: "...", user_permissions: {...} }
chat.context = { page: "lists#show", selected_list_id: "..." }
Migrations
Run migrations with:
# Create databases
rails db:create
# Run all migrations
rails db:migrate
# For specific databases
rails db:migrate:cache
rails db:migrate:queue
rails db:migrate:cable
# Rollback
rails db:rollback STEP=1
Audit Trail & Change History
Listopia uses Logidze for comprehensive audit logging and temporal queries. This provides a complete change history without additional tables per model.
How Logidze Works
Logidze stores changes as a JSON log in the logidze_data table and automatically triggers PostgreSQL functions to record mutations:
# Add to any model to track changes
class List < ApplicationRecord
has_logidze
end
Accessing Change History
# Get all changes to a list
list.log_data.map { |v| v.version }
# Get specific version at a point in time
list_v1 = list.at(2.days.ago)
# Compare current vs historical
list.changes_from(1.day.ago)
# View who changed what
list.log_data.diff # JSON of all diffs
Temporal Queries
# Find lists as they existed 3 days ago
List.at(3.days.ago)
# Get audit information
list.logidze_audit_log
Audited Models
Models with has_logidze are tracked:
- List - Track title, description, status changes
- ListItem - Track item status, assignments, priority changes
- Chat - Track conversation state and context updates
Schema Considerations
- Uses SQL format (
config.active_record.schema_format = :sql) to preserve database functions and triggers - Logidze data stored in
logidze_datatable with JSONB log_data - Minimal performance overhead with async cleanup
Performance Notes
- Eager loading - Always use
.includes()to avoid N+1 queries - Pagination - Use
pagygem for efficient list pagination - Full-text search -
pg_searchgem for PostgreSQL search capabilities - Soft deletes -
discardgem for recoverable deletions - Audit trail -
logidzegem tracks all changes with temporal queries and complete change history