Skip to main content

Database Architecture

Last Updated: 2026-01-18

Aragora uses a dual-storage architecture: SQLite for local development and persistent state, Supabase for cloud deployment. PostgreSQL is supported for production deployments.

DocumentPurpose
DATABASE.md (this)Architecture and configuration
DATABASE_SETUP.mdInstallation and setup guide
ENVIRONMENT.mdEnvironment variables reference

Quick Reference

AspectValue
Default Modelegacy (multiple SQLite files)
Consolidated Modeconsolidated (4 databases, optional)
PostgreSQLFull support via psycopg2
Connection PoolingBuilt-in (10-20 per DB)

Consolidated Databases (Default)

DatabasePurposeKey Tables
core.dbDebates, traces, tournamentsdebates, traces, positions, embeddings
memory.dbAgent memory systemscontinuum_memory, consensus, patterns
analytics.dbELO ratings, insightsratings, matches, elo_history, insights
agents.dbAgent personas, genesispersonas, genomes, relationships

Legacy Databases (multiple individual files)

DatabasePurposeDefault Path
agent_elo.dbAgent ELO rankingsARAGORA_DATA_DIR (default .nomic/)
continuum.dbContinuum memory tiersARAGORA_DATA_DIR (default .nomic/)
consensus_memory.dbConsensus historyARAGORA_DATA_DIR (default .nomic/)
agora_memory.dbCritiqueStore patterns (CLI)ARAGORA_DATA_DIR (default .nomic/)
agent_calibration.dbCalibration trackingARAGORA_DATA_DIR (default .nomic/)
aragora_insights.dbInsights and analyticsARAGORA_DATA_DIR (default .nomic/)
agent_personas.dbPersonasARAGORA_DATA_DIR (default .nomic/)
grounded_positions.dbTruth-grounded positionsARAGORA_DATA_DIR (default .nomic/)
genesis.dbGenesis ledger / genomesARAGORA_DATA_DIR (default .nomic/)
token_blacklist.dbJWT revocation storeARAGORA_DATA_DIR (default .nomic/)
users.dbUser/org accountsARAGORA_DATA_DIR (default .nomic/)
explainability_batch_jobs.dbBatch explainability job stateARAGORA_DATA_DIR (default .nomic/)

Other legacy files are created on demand (e.g., persona_lab.db, agent_relationships.db). Use ARAGORA_DB_* overrides or aragora.config.legacy.DB_NAMES for the full list.


Database Mode Configuration

# Use consolidated mode (optional)
export ARAGORA_DB_MODE=consolidated

# Use legacy mode (default)
export ARAGORA_DB_MODE=legacy

Programmatic Access

from aragora.persistence.db_config import (
get_db_path,
DatabaseType,
get_db_mode,
DatabaseMode,
)

# Get path for a specific database type
elo_path = get_db_path(DatabaseType.ELO) # -> analytics.db in consolidated mode

# Check current mode
mode = get_db_mode()
if mode == DatabaseMode.CONSOLIDATED:
print("Using consolidated databases")

Migration: Legacy to Consolidated

Running the Migration

# 1. Dry run - see what would be migrated
python -m aragora.persistence.migrations.consolidate --dry-run --source .nomic

# 2. Execute migration (creates backup automatically)
python -m aragora.persistence.migrations.consolidate --migrate --source .nomic

# 3. Verify migration
python -m aragora.persistence.migrations.consolidate --verify --source .nomic

Migration Options

OptionDescription
--dry-runShow what would be migrated
--migrateExecute the consolidation
--verifyVerify consolidated databases
--source DIRSource directory (default: .nomic)
--target DIRTarget directory (default: same as source)
--no-backupSkip backup (not recommended)

Backups are created at .nomic/backup/YYYYMMDD_HHMMSS/


PostgreSQL Production Setup

For high-concurrency or distributed deployments.

Prerequisites

pip install psycopg2-binary  # or psycopg2 for production

Environment Configuration

# Required
export DATABASE_URL="postgresql://user:password@host:5432/aragora"

# Optional (pool tuning)
export ARAGORA_DB_POOL_SIZE=10 # Pool size
export ARAGORA_DB_POOL_MAX_OVERFLOW=5 # Extra connections
export ARAGORA_DB_POOL_TIMEOUT=30 # Connection timeout

ARAGORA_DATABASE_URL is accepted as a legacy alias for DATABASE_URL.

Connection String Format

postgresql://[user]:[password]@[host]:[port]/[database]

# Examples:
postgresql://aragora:secret@localhost:5432/aragora_prod
postgresql://user:pass@db.example.com:5432/aragora?sslmode=require

Database Setup

-- Create database
CREATE DATABASE aragora_prod;
CREATE USER aragora WITH ENCRYPTED PASSWORD 'your-secure-password';
GRANT ALL PRIVILEGES ON DATABASE aragora_prod TO aragora;

Apply Schemas

psql -U aragora -d aragora_prod -f aragora/persistence/schemas/core.sql
psql -U aragora -d aragora_prod -f aragora/persistence/schemas/memory.sql
psql -U aragora -d aragora_prod -f aragora/persistence/schemas/analytics.sql
psql -U aragora -d aragora_prod -f aragora/persistence/schemas/agents.sql

Encryption

Encryption at Rest

Data at rest encryption protects database files from unauthorized access if storage media is compromised.

SQLite Encryption

Option 1: SQLCipher (Recommended for sensitive data)

SQLCipher provides transparent 256-bit AES encryption for SQLite databases.

# Install SQLCipher
pip install sqlcipher3

# Or use system SQLCipher
brew install sqlcipher # macOS
apt-get install sqlcipher # Debian/Ubuntu

Configuration:

export ARAGORA_SQLITE_ENCRYPTION=1
export ARAGORA_SQLITE_KEY="your-32-character-encryption-key"

Usage:

from aragora.storage.encrypted import get_encrypted_connection

# Opens database with encryption
conn = get_encrypted_connection("/path/to/secure.db")

Option 2: OS-Level Encryption

For less sensitive deployments, use filesystem-level encryption:

  • macOS: FileVault (enabled by default)
  • Linux: LUKS dm-crypt for volume encryption
  • Windows: BitLocker
# Linux LUKS example
cryptsetup luksFormat /dev/sdb1
cryptsetup luksOpen /dev/sdb1 aragora_data
mkfs.ext4 /dev/mapper/aragora_data
mount /dev/mapper/aragora_data /var/lib/aragora

PostgreSQL Encryption

Transparent Data Encryption (TDE)

For PostgreSQL 16+, use TDE for full database encryption:

-- Enable TDE (requires PostgreSQL Enterprise or contrib)
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Encrypt sensitive columns
ALTER TABLE users
ALTER COLUMN password_hash
SET DATA TYPE bytea
USING pgp_sym_encrypt(password_hash::text, current_setting('app.encryption_key'))::bytea;

AWS RDS Encryption

For AWS-hosted PostgreSQL:

# Enable encryption when creating RDS instance
aws rds create-db-instance \
--db-instance-identifier aragora-prod \
--storage-encrypted \
--kms-key-id alias/aws/rds

Column-Level Encryption

For specific sensitive fields:

from cryptography.fernet import Fernet

# Generate key (store securely)
key = Fernet.generate_key()
cipher = Fernet(key)

# Encrypt before storage
encrypted_data = cipher.encrypt(sensitive_data.encode())

# Decrypt when reading
decrypted_data = cipher.decrypt(encrypted_data).decode()

Redis Encryption

TLS for Redis Connections

# Use rediss:// scheme for TLS
export REDIS_URL="rediss://user:password@redis-host:6379/0"

Redis configuration (redis.conf):

tls-port 6379
port 0
tls-cert-file /path/to/redis.crt
tls-key-file /path/to/redis.key
tls-ca-cert-file /path/to/ca.crt

AWS ElastiCache Encryption

# Enable encryption in transit and at rest
aws elasticache create-replication-group \
--replication-group-id aragora-cache \
--transit-encryption-enabled \
--at-rest-encryption-enabled

Encryption in Transit

All database connections should use encrypted transport:

DatabaseProtocolConfiguration
PostgreSQLTLS/SSL?sslmode=require in connection string
RedisTLSrediss:// scheme
SupabaseHTTPSAutomatic (always encrypted)

PostgreSQL SSL configuration:

export DATABASE_URL="postgresql://user:pass@host:5432/db?sslmode=verify-full&sslrootcert=/path/to/ca.crt"

Key Management

Environment Variables (Development)

export ARAGORA_ENCRYPTION_KEY="base64-encoded-32-byte-key"
export ARAGORA_SQLITE_KEY="another-encryption-key"

Kubernetes Secrets (Production)

apiVersion: v1
kind: Secret
metadata:
name: aragora-encryption
type: Opaque
stringData:
encryption-key: "your-base64-encoded-key"
sqlite-key: "your-sqlite-encryption-key"

External Secret Managers

AWS Secrets Manager:

import boto3

def get_encryption_key():
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='aragora/encryption-key')
return response['SecretString']

HashiCorp Vault:

import hvac

def get_encryption_key():
client = hvac.Client(url='https://vault.example.com')
secret = client.secrets.kv.read_secret_version(path='aragora/encryption')
return secret['data']['data']['key']

Key Rotation

Rotating SQLCipher Keys

# Export data with old key
sqlite3 encrypted.db "PRAGMA key = 'old-key'; .dump" > backup.sql

# Create new database with new key
sqlite3 new_encrypted.db "PRAGMA key = 'new-key';"
sqlite3 new_encrypted.db < backup.sql

# Replace old database
mv new_encrypted.db encrypted.db

Rotating PostgreSQL Keys

For column-level encryption, re-encrypt data:

from cryptography.fernet import Fernet

old_cipher = Fernet(old_key)
new_cipher = Fernet(new_key)

# Re-encrypt all sensitive data
for row in get_all_encrypted_rows():
decrypted = old_cipher.decrypt(row.encrypted_data)
new_encrypted = new_cipher.encrypt(decrypted)
update_row(row.id, new_encrypted)

Local Development (SQLite)

Database Location

By default, databases are stored in ARAGORA_DATA_DIR (default .nomic/). Override with:

export ARAGORA_DATA_DIR=/path/to/data

Legacy aliases:

export ARAGORA_NOMIC_DIR=/path/to/data  # used by some migration tooling
export NOMIC_DIR=/path/to/data # older env var

If you ran Aragora in the repo root and created stray .db files, move them under ARAGORA_DATA_DIR with:

scripts/cleanup_runtime_artifacts.sh

Connection Configuration

from aragora.storage.schema import get_wal_connection

# Default connection with WAL mode
conn = get_wal_connection("/path/to/database.db")

# With custom timeout
conn = get_wal_connection("/path/to/database.db", timeout=60.0)

WAL (Write-Ahead Logging) mode is enabled by default for:

  • Better concurrent read/write performance
  • Improved crash recovery
  • Non-blocking reads during writes

Database Stores

StoreModulePurpose
UserStorearagora.storage.user_storeUser accounts, auth
OrganizationStorearagora.storage.organization_storeMulti-tenant orgs
PolicyStorearagora.compliance.policy_storePolicy rules, violations (SQLite/Postgres)
AuditLogaragora.audit.logImmutable audit events + exports (SQLite/Postgres)
ShareLinkStorearagora.storage.share_storeShared debate links
WebhookStorearagora.storage.webhook_storeWebhook configs
from aragora.storage.user_store import UserStore

store = UserStore(db_path="/path/to/users.db")
user = store.get_by_email("user@example.com")

Audit & Policy Backends:

  • Set ARAGORA_AUDIT_STORE_BACKEND=postgres or ARAGORA_POLICY_STORE_BACKEND=postgres to force Postgres for those stores.
  • Both inherit ARAGORA_DB_BACKEND when not explicitly set.

Cloud Deployment (Supabase)

Setup

  1. Create a Supabase project

  2. Set environment variables:

    export SUPABASE_URL=https://yourproject.supabase.co
    export SUPABASE_KEY=your-service-role-key
  3. Run schema migrations (see SQL files in supabase/migrations/)

Configuration Check

from aragora.persistence.supabase_client import SupabaseClient

client = SupabaseClient()
print(f"Configured: {client.is_configured}")

Supabase Tables

TablePurpose
nomic_cyclesNomic loop cycle state
debate_artifactsDebate transcripts
stream_eventsReal-time events
agent_metricsAgent performance
nomic_rollbacksRollback history
cycle_evolutionCodebase evolution
cycle_file_changesFile change tracking

Schema

Users Table

CREATE TABLE users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
password_salt TEXT NOT NULL,
name TEXT DEFAULT '',
org_id TEXT,
role TEXT DEFAULT 'member',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_login_at TEXT,
is_active INTEGER DEFAULT 1,
email_verified INTEGER DEFAULT 0,
avatar_url TEXT,
preferences TEXT DEFAULT '{}',
-- Added in migration 002
locked_until TEXT,
failed_login_count INTEGER DEFAULT 0,
lockout_reason TEXT,
last_activity_at TEXT,
last_debate_at TEXT
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_org ON users(org_id);

Organizations Table

CREATE TABLE organizations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
tier TEXT DEFAULT 'free',
owner_id TEXT REFERENCES users(id),
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
settings TEXT DEFAULT '{}'
);

CREATE INDEX idx_orgs_slug ON organizations(slug);
CREATE INDEX idx_orgs_stripe ON organizations(stripe_customer_id);

Audit Log Table

CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
user_id TEXT,
org_id TEXT,
action TEXT NOT NULL,
resource_type TEXT,
resource_id TEXT,
details TEXT DEFAULT '{}',
ip_address TEXT,
user_agent TEXT
);

CREATE INDEX idx_audit_user ON audit_log(user_id);
CREATE INDEX idx_audit_org ON audit_log(org_id);
CREATE INDEX idx_audit_timestamp ON audit_log(timestamp);

Usage Events Table

CREATE TABLE usage_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
org_id TEXT NOT NULL REFERENCES organizations(id),
event_type TEXT NOT NULL,
count INTEGER DEFAULT 1,
metadata TEXT DEFAULT '{}',
timestamp TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_usage_org ON usage_events(org_id);
CREATE INDEX idx_usage_timestamp ON usage_events(timestamp);

Migrations

Running Migrations

# Check migration status
python -m aragora.persistence.migrations.runner --status

# Dry-run (show what would be done)
python -m aragora.persistence.migrations.runner --dry-run

# Run all pending migrations
python -m aragora.persistence.migrations.runner --migrate

# Run migrations for specific database
python -m aragora.persistence.migrations.runner --migrate --db users

Creating Migrations

# Create new migration
python -m aragora.persistence.migrations.runner --create "Add user lockout fields" --db users

This creates a file like aragora/persistence/migrations/users/00x_add_user_lockout_fields.py:

"""
Migration 00x: Add user lockout fields

Created: 2024-01-03T12:00:00
"""

import sqlite3

def upgrade(conn: sqlite3.Connection) -> None:
"""Apply this migration."""
conn.execute("""
ALTER TABLE users ADD COLUMN locked_until TEXT
""")

def downgrade(conn: sqlite3.Connection) -> None:
"""Reverse this migration (optional)."""
# SQLite has limited ALTER TABLE support
pass

Migration File Naming

Migration files follow the pattern: NNN_description.py

  • 001_initial.py - Initial schema
  • 002_add_lockout.py - Add lockout fields
  • 003_add_analytics.py - Add analytics

Safe Column Addition

Use safe_add_column to handle existing columns:

from aragora.storage.schema import safe_add_column

def upgrade(conn: sqlite3.Connection) -> None:
# Won't fail if column already exists
safe_add_column(conn, "users", "new_field", "TEXT", "NULL")

Backup and Restore

SQLite Backup

# Manual backup
cp "${ARAGORA_DATA_DIR:-.nomic}/users.db" \
"${ARAGORA_DATA_DIR:-.nomic}/backups/users_$(date +%Y%m%d).db"

# Using sqlite3 backup command
sqlite3 "${ARAGORA_DATA_DIR:-.nomic}/users.db" \
".backup '${ARAGORA_DATA_DIR:-.nomic}/backups/users.db'"

Automated Backup Script

#!/bin/bash
# scripts/backup_dbs.sh

BACKUP_DIR="${ARAGORA_DATA_DIR:-.nomic}/backups"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p "$BACKUP_DIR"

for db in agent_elo.db continuum.db consensus_memory.db agent_calibration.db \
aragora_insights.db agent_personas.db grounded_positions.db genesis.db \
token_blacklist.db users.db agora_memory.db; do
src="${ARAGORA_DATA_DIR:-.nomic}/$\{db\}"
if [ -f "$src" ]; then
sqlite3 "$src" ".backup '$BACKUP_DIR/${db%.db}_$\{DATE\}.db'"
echo "Backed up: $db"
fi
done

# Cleanup old backups (keep last 7 days)
find "$BACKUP_DIR" -name "*.db" -mtime +7 -delete

Restore from Backup

# Stop the server first
cp "${ARAGORA_DATA_DIR:-.nomic}/backups/users_20240101.db" \
"${ARAGORA_DATA_DIR:-.nomic}/users.db"

Performance Monitoring

Slow Query Logging

Set threshold in milliseconds:

export ARAGORA_SLOW_QUERY_MS=500

Queries exceeding this threshold are logged:

WARNING: Slow query (0.523s): save_cycle [cycle=5] (threshold: 0.500s)

Connection Pooling

For high-concurrency scenarios, use connection pools:

from aragora.storage.schema import DatabaseManager

# Singleton manager handles connection pooling
manager = DatabaseManager.get_instance("users", "/path/to/users.db")
conn = manager.get_connection()

Query Optimization

  1. Use indexes - All foreign keys and commonly queried columns should be indexed
  2. Batch operations - Use executemany() for bulk inserts
  3. Prepared statements - Use parameterized queries
  4. WAL mode - Enabled by default for concurrent access

Data Retention

Audit Log Retention

Configure retention in aragora/storage/audit_store.py:

# Default: 90 days
AUDIT_RETENTION_DAYS = int(os.getenv("ARAGORA_AUDIT_RETENTION_DAYS", "90"))

Cleanup old audit entries:

from aragora.storage.audit_store import AuditStore

store = AuditStore(db_path)
store.cleanup_old_entries() # Uses AUDIT_RETENTION_DAYS

Usage Event Aggregation

Usage events are aggregated monthly and raw events older than 90 days are pruned.

Troubleshooting

Database Locked

If you see "database is locked" errors:

  1. Check for long-running queries
  2. Ensure WAL mode is enabled
  3. Increase timeout: DB_TIMEOUT=60
# Check WAL mode
conn = get_wal_connection("/path/to/db.db")
cursor = conn.execute("PRAGMA journal_mode")
print(cursor.fetchone()) # Should be ('wal',)

Schema Version Mismatch

If migration fails with version mismatch:

# Check current version
python -m aragora.persistence.migrations.runner --status

# View schema_version table directly
sqlite3 "${ARAGORA_DATA_DIR:-.nomic}/users.db" "SELECT * FROM schema_version"

Corrupted Database

If database is corrupted:

  1. Restore from backup
  2. Or try SQLite recovery:
    sqlite3 corrupted.db ".dump" | sqlite3 recovered.db

Connection Issues (Supabase)

Verify configuration:

import os
print(f"URL: {os.getenv('SUPABASE_URL')}")
print(f"Key set: {bool(os.getenv('SUPABASE_KEY'))}")

from aragora.persistence.supabase_client import SupabaseClient
client = SupabaseClient()
print(f"Configured: {client.is_configured}")

Data Models

NomicCycle

@dataclass
class NomicCycle:
loop_id: str
cycle_number: int
phase: str # debate, design, implement, verify, commit
stage: str # proposing, critiquing, voting, executing
started_at: datetime
completed_at: Optional[datetime] = None
success: Optional[bool] = None
git_commit: Optional[str] = None
task_description: Optional[str] = None
total_tasks: int = 0
completed_tasks: int = 0
error_message: Optional[str] = None

DebateArtifact

@dataclass
class DebateArtifact:
loop_id: str
cycle_number: int
phase: str
task: str
agents: list[str]
transcript: list[dict] # Full message history
consensus_reached: bool
confidence: float
winning_proposal: Optional[str] = None
vote_tally: Optional[dict] = None

StreamEvent

@dataclass
class StreamEvent:
loop_id: str
cycle: int
event_type: str # cycle_start, phase_start, task_complete, error
event_data: dict
agent: Optional[str] = None
timestamp: datetime = field(default_factory=datetime.utcnow)

Environment Variables

VariableDefaultDescription
ARAGORA_DATA_DIR.nomicDatabase directory
ARAGORA_NOMIC_DIR.nomicLegacy alias for some migration tooling
SUPABASE_URL-Supabase project URL
SUPABASE_KEY-Supabase service key
ARAGORA_SLOW_QUERY_MS500Slow query threshold
ARAGORA_AUDIT_RETENTION_DAYS90Audit log retention
DB_TIMEOUT30SQLite connection timeout