Relevant source files
The following files were used as context for generating this wiki page:
- server/models.py
- integrations/mem0-agent-plugin/core/memory_core.py
- server/alembic/versions/001_create_users_and_api_keys.py
- integrations/agent-plugin-core/python/memory_core.py
- integrations/claude-code-plugin/core/memory_core.py
- integrations/antigravity-plugin/core/memory_core.py
- integrations/kimi-plugin/core/memory_core.py
- server/alembic/env.py
- integrations/codex-plugin/core/memory_core.py
- server/routers/auth.py
- server/main.py
- server/alembic/versions/002_create_request_logs.py
- integrations/cursor-plugin/core/memory_core.py
- server/routers/api_keys.py
- server/db.py
- server/alembic/versions/003_create_settings.py
- server/auth.py
- server/alembic.ini
- mem0/memory/storage.py
- server/alembic/versions/005_refresh_token_jtis.py
- server/routers/requests.py
- server/server_state.py
- server/alembic/versions/006_request_logs_brin.py
- server/scripts/prune_request_logs.py
- mem0/configs/vector_stores/oracledb.py
- mem0/configs/base.py
- server/init-db.sh
- server/alembic/versions/004_unique_admin_role.py
Overview
The relational database architecture relies on SQLAlchemy Object-Relational Mapping (ORM) and Alembic migrations to manage persistence, connection pooling, and schema evolution for user authentication, API keys, audit logs, and system settings. Using declarative base classes and dynamic connection string resolution, the engine connects to PostgreSQL instances while maintaining session lifecycles through FastAPI dependency injection.
Sources: server/db.py:1-31
Alembic coordinates schema versioning through structured migration scripts that track incremental database modifications, supporting both offline SQL generation and online transactional execution against target metadata.
Sources: server/alembic/env.py:1-44, server/alembic/versions/001_create_users_and_api_keys.py:1-49
Database Engine Configuration and Session Lifecycle
Dynamic URL Resolution and Engine Initialization
The database engine constructs its connection string dynamically by reading environment variables with default fallback values. The _build_database_url() function inspects POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, and APP_DB_NAME, falling back respectively to postgres, 5432, postgres, postgres, and mem0_app. It formats these components into a PostgreSQL connection URI using the postgresql+psycopg driver scheme.
def _build_database_url() -> str:
host = os.environ.get("POSTGRES_HOST", "postgres")
port = os.environ.get("POSTGRES_PORT", "5432")
user = os.environ.get("POSTGRES_USER", "postgres")
password = os.environ.get("POSTGRES_PASSWORD", "postgres")
db = os.environ.get("APP_DB_NAME", "mem0_app")
return f"postgresql+psycopg://{user}:{password}@{host}:{port}/{db}"Sources: server/db.py:7-13
The global engine instance is created by passing the generated database URL to create_engine() with pool_pre_ping=True enabled to verify connection health before checkout.
engine = create_engine(_build_database_url(), pool_pre_ping=True)Sources: server/db.py:16-16
Note
The database initialization script (init-db.sh) independently provisions the mem0_app application database using psql conditional execution (WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'mem0_app')), while the default postgres database is reserved for pgvector memory storage.
Sources: server/init-db.sh:4-9
Session Factory and Dependency Injection
Session management relies on SessionLocal, configured as a sessionmaker bound to the global engine with autoflush=False and expire_on_commit=False.
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)Sources: server/db.py:18-18
Models inherit from a declarative base class Base constructed using DeclarativeBase.
class Base(DeclarativeBase):
passSources: server/db.py:21-23
The get_db() FastAPI dependency yields a transactional Session instance and guarantees closure in a finally block regardless of request outcomes.
def get_db():
"""FastAPI dependency that yields a SQLAlchemy session."""
db: Session = SessionLocal()
try:
yield db
finally:
db.close()Sources: server/db.py:25-31
Warning
Failing to consume get_db() within a FastAPI route or background task dependency injection context leaves connections unreturned to the pool, risking connection exhaustion under concurrent request load.
Sources: server/db.py:25-31
Identity and Authentication Models
User Accounts and Role Constraints
The User model defines the primary authentication entity managed within the application database (server/models.py). Each user record utilizes a UUID primary key, stores a string representation of their name and email, and retains a hashed password string alongside role permissions.
class User(Base):
__tablename__ = "users"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=_new_uuid)
name: Mapped[str] = mapped_column(String(255))
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(Text)
role: Mapped[str] = mapped_column(String(20), default="admin")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)Sources: server/models.py:18-28
Database schema evolution supports administrative scoping via migration 004_unique_admin_role.py, which applies a partial unique index on the role column where the role equals 'admin'. This ensures at most one administrator account can exist concurrently.
def upgrade() -> None:
op.create_index(
"ix_users_only_one_admin",
"users",
["role"],
unique=True,
postgresql_where=sa.text("role = 'admin'"),
)Sources: server/alembic/versions/004_unique_admin_role.py:20-27
Warning
Attempting to insert a second user with the admin role directly into PostgreSQL violates the ix_users_only_one_admin partial unique index constraint, triggering a database integrity error.
Sources: server/alembic/versions/004_unique_admin_role.py:20-27
API Keys and Relational Mappings
The APIKey model manages programmatic access credentials linked to specific user accounts. Each key entry stores a short prefix for identification, a secure cryptographic hash of the full key value, an administrative label, and timestamps tracking usage and revocation.
class APIKey(Base):
__tablename__ = "api_keys"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=_new_uuid)
key_prefix: Mapped[str] = mapped_column(String(12))
key_hash: Mapped[str] = mapped_column(Text)
label: Mapped[str] = mapped_column(String(255))
created_by: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)Sources: server/models.py:30-41
The initial schema migration establishes both the users and api_keys tables concurrently, enforcing foreign key cascades on deletion.
op.create_table(
"api_keys",
sa.Column("id", sa.Uuid(), primary_key=True),
sa.Column("key_prefix", sa.String(12), nullable=False),
sa.Column("key_hash", sa.Text(), nullable=False),
sa.Column("label", sa.String(255), nullable=False),
sa.Column("created_by", sa.Uuid(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False),
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
)Sources: server/alembic/versions/001_create_users_and_api_keys.py:34-44
Single-Use Refresh Token JTIs
The RefreshTokenJti model implements storage for JWT JWT-ID (jti) claims to enforce single-use refresh token semantics.
class RefreshTokenJti(Base):
__tablename__ = "refresh_token_jtis"
jti: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=_new_uuid)
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)Sources: server/models.py:55-63
Migration 005_refresh_token_jtis.py adds this table along with an expiration index to accelerate token cleanups and validation lookups.
def upgrade() -> None:
op.create_table(
"refresh_token_jtis",
sa.Column("jti", sa.Uuid(), primary_key=True),
sa.Column(
"user_id",
sa.Uuid(),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_refresh_token_jtis_expires_at", "refresh_token_jtis", ["expires_at"])Sources: server/alembic/versions/005_refresh_token_jtis.py:20-34
Important
When a refresh token is successfully exchanged, its corresponding used_at timestamp is populated, preventing any subsequent reuse attempt even if the token remains cryptographically valid before its expires_at threshold.
Sources: server/models.py:55-63, server/alembic/versions/005_refresh_token_jtis.py:20-34
Request Logging and Settings Models
Request Logging Model and Migration
The RequestLog SQLAlchemy model captures telemetry for incoming HTTP requests Server API Endpoints. Each log record tracks the HTTP method, request path, response status code, latency in milliseconds, authentication mechanism used, and creation timestamp.
class RequestLog(Base):
__tablename__ = "request_logs"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=_new_uuid)
method: Mapped[str] = mapped_column(String(16))
path: Mapped[str] = mapped_column(String(512))
status_code: Mapped[int] = mapped_column(Integer)
latency_ms: Mapped[float] = mapped_column(Float)
auth_type: Mapped[str] = mapped_column(String(32), default="none")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)Sources: server/models.py:43-53
Migration 002_create_request_logs.py provisions the request_logs relation and creates a B-tree index on created_at to support time-range queries and pruning operations.
def upgrade() -> None:
op.create_table(
"request_logs",
sa.Column("id", sa.Uuid(), primary_key=True),
sa.Column("method", sa.String(16), nullable=False),
sa.Column("path", sa.String(512), nullable=False),
sa.Column("status_code", sa.Integer(), nullable=False),
sa.Column("latency_ms", sa.Float(), nullable=False),
sa.Column("auth_type", sa.String(32), nullable=False, server_default="none"),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
)
op.create_index("ix_request_logs_created_at", "request_logs", ["created_at"])Sources: server/alembic/versions/002_create_request_logs.py:20-31
Note
The auth_type column defaults to "none" at the database schema level via a server default expression, ensuring unauthenticated or public routes are explicitly labeled even when no credential extractor populates the context.
Sources: server/models.py:43-53, server/alembic/versions/002_create_request_logs.py:28-28
Settings Model and Migration
The Settings model defines a key-value store used to persist dynamic application configurations Dashboard Views. The model uses key as the primary key string and updates the updated_at timestamp automatically via SQLAlchemy's onupdate hook.
class Settings(Base):
__tablename__ = "settings"
key: Mapped[str] = mapped_column(String(255), primary_key=True)
value: Mapped[str] = mapped_column(Text)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=_utcnow,
onupdate=_utcnow,
)Sources: server/models.py:65-74
Migration 003_create_settings.py creates the settings table with a corresponding updated_at server timestamp default.
def upgrade() -> None:
op.create_table(
"settings",
sa.Column("key", sa.String(255), primary_key=True),
sa.Column("value", sa.Text(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
)Sources: server/alembic/versions/003_create_settings.py:20-26
Alembic Migration Environment and Versioning
Configuration and Metadata Initialization
The Alembic migration environment is configured via server/alembic.ini and orchestrated by server/alembic/env.py. The alembic.ini file sets the default script location to alembic, prepends the current working directory to sys.path, and defines a default PostgreSQL connection string pointing to postgresql+psycopg://postgres:postgres@postgres:5432/mem0_app.
Sources: server/alembic.ini:1-4
[alembic]
script_location = alembic
prepend_sys_path = .
sqlalchemy.url = postgresql+psycopg://postgres:postgres@postgres:5432/mem0_appSources: server/alembic.ini:1-4
The env.py script imports Base and _build_database_url from db, along with the models module to ensure Base.metadata registers all relational table definitions. It configures logging handlers from alembic.ini and dynamically overrides sqlalchemy.url using _build_database_url().
Sources: server/alembic/env.py:1-20
from db import Base, _build_database_url
import models # noqa: F401
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
config.set_main_option("sqlalchemy.url", _build_database_url())Sources: server/alembic/env.py:6-20
Offline and Online Runner Execution
Alembic execution branches depending on whether context.is_offline_mode() evaluates to true. In offline mode, migrations generate SQL scripts without establishing a live database connection, configuring context with literal_binds=True.
Sources: server/alembic/env.py:22-27
def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
with context.begin_transaction():
context.run_migrations()Sources: server/alembic/env.py:22-27
In online mode, Alembic builds an engine using engine_from_config with poolclass=pool.NullPool, acquires a live connection, and executes migration operations within a transactional block.
Sources: server/alembic/env.py:29-39
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()Sources: server/alembic/env.py:29-39
Note
Database initialization is bootstrapped prior to migration runs using the server/init-db.sh script, which executes an idempotent psql command to create the mem0_app database if it does not already exist.
Sources: server/init-db.sh:1-9
Schema Evolution and Index Optimization
Migration History Across Releases
The database schema evolves through six sequential Alembic migration revisions that progressively establish core application tables, audit logs, dynamic settings, authentication components, administrative constraints, and storage optimizations. Sources: server/alembic/versions/001_create_users_and_api_keys.py:1-49, server/alembic/versions/002_create_request_logs.py:1-36, server/alembic/versions/003_create_settings.py:1-30, server/alembic/versions/004_unique_admin_role.py:1-31, server/alembic/versions/005_refresh_token_jtis.py:1-39, server/alembic/versions/006_request_logs_brin.py:1-26
| Revision ID | Revises | Description | Core Tables / Operations |
|---|---|---|---|
001 | None | Create users and api_keys tables | users, api_keys, ix_users_email |
002 | 001 | Create request_logs table | request_logs, ix_request_logs_created_at |
003 | 002 | Create settings table | settings |
004 | 003 | Enforce at-most-one admin via partial unique index | ix_users_only_one_admin (postgresql_where=sa.text("role = 'admin'")) |
005 | 004 | Create refresh_token_jtis table for single-use refresh tokens | refresh_token_jtis, ix_refresh_token_jtis_expires_at |
006 | 005 | Swap request_logs.created_at btree index for a BRIN index | Drop ix_request_logs_created_at, create BRIN index on request_logs(created_at) |
Sources: server/alembic/versions/001_create_users_and_api_keys.py:1-49, server/alembic/versions/002_create_request_logs.py:1-36, server/alembic/versions/003_create_settings.py:1-30, server/alembic/versions/004_unique_admin_role.py:1-31, server/alembic/versions/005_refresh_token_jtis.py:1-39, server/alembic/versions/006_request_logs_brin.py:1-26
Role Constraints and Partial Indexing
Revision 004 introduces a restrictive uniqueness invariant on user roles to ensure that at most one administrator account can exist within the system simultaneously. This is implemented via a PostgreSQL partial unique index named ix_users_only_one_admin scoped exclusively to rows where role = 'admin'.
Sources: server/alembic/versions/004_unique_admin_role.py:20-27
def upgrade() -> None:
op.create_index(
"ix_users_only_one_admin",
"users",
["role"],
unique=True,
postgresql_where=sa.text("role = 'admin'"),
)Sources: server/alembic/versions/004_unique_admin_role.py:20-27
Important
The partial unique index ix_users_only_one_admin relies on PostgreSQL-specific postgresql_where syntax. Attempting to run migration 004 on a non-PostgreSQL backend will fail because standard SQL does not support filtered unique indexes in the same manner.
Sources: server/alembic/versions/004_unique_admin_role.py:20-27
Swapping B-tree for BRIN Indexes on Time-Series Logs
The request_logs table accumulates high-volume append-only telemetry data ordered naturally by insertion timestamp. Initially created with a standard B-tree index on created_at in revision 002, migration 006 explicitly drops ix_request_logs_created_at and replaces it with a Block Range Index (BRIN) via raw SQL execution.
Sources: server/alembic/versions/002_create_request_logs.py:31-31, server/alembic/versions/006_request_logs_brin.py:19-21
def upgrade() -> None:
op.drop_index("ix_request_logs_created_at", table_name="request_logs")
op.execute("CREATE INDEX ix_request_logs_created_at ON request_logs USING BRIN (created_at)")Sources: server/alembic/versions/006_request_logs_brin.py:19-21
Tip
BRIN indexes consume a fraction of the disk space required by B-tree indexes on large append-only time-series tables like request_logs, because they store summary metadata per physical page range instead of maintaining individual keys per row.
Sources: server/alembic/versions/006_request_logs_brin.py:19-21
Log Retention and Maintenance Automation
Retention Window Policies and Execution
The server maintains audit telemetry through a dedicated pruning utility designed to purge old records from the request_logs table based on a configurable age threshold. The retention duration is controlled by the REQUEST_LOG_RETENTION_DAYS environment variable, which defaults to 30 days if left unspecified or empty.
Sources: server/scripts/prune_request_logs.py:1-17
def main() -> int:
raw = os.environ.get("REQUEST_LOG_RETENTION_DAYS", "").strip() or "30"
try:
retention_days = int(raw)
except ValueError:
sys.stderr.write("REQUEST_LOG_RETENTION_DAYS must be an integer.\n")
return 1
if retention_days < 1:
sys.stderr.write("REQUEST_LOG_RETENTION_DAYS must be >= 1.\n")
return 1
cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
with SessionLocal() as session:
result = session.execute(delete(RequestLog).where(RequestLog.created_at < cutoff))
session.commit()
sys.stdout.write(f"Deleted {result.rowcount or 0} request_logs rows older than {cutoff.isoformat()}.\n")
return 0Sources: server/scripts/prune_request_logs.py:16-32
Caution
The pruning script strictly validates that REQUEST_LOG_RETENTION_DAYS parses to an integer greater than or equal to 1. Providing a non-integer value or a number less than 1 prints an error message to standard error and terminates execution with exit code 1.
Sources: server/scripts/prune_request_logs.py:18-25
The maintenance script can be executed directly inside the container or integrated into external scheduling facilities such as cron or systemd timers on the host system. Sources: server/scripts/prune_request_logs.py:3-3
Related
On This Page
OverviewDatabase Engine Configuration and Session LifecycleDynamic URL Resolution and Engine InitializationSession Factory and Dependency InjectionIdentity and Authentication ModelsUser Accounts and Role ConstraintsAPI Keys and Relational MappingsSingle-Use Refresh Token JTIsRequest Logging and Settings ModelsRequest Logging Model and MigrationSettings Model and MigrationAlembic Migration Environment and VersioningConfiguration and Metadata InitializationOffline and Online Runner ExecutionSchema Evolution and Index OptimizationMigration History Across ReleasesRole Constraints and Partial IndexingSwapping B-tree for BRIN Indexes on Time-Series LogsLog Retention and Maintenance AutomationRetention Window Policies and ExecutionRelated