Data and Persistence
This document describes how persistent state is stored and accessed: PostgreSQL (engine, models, query layer), optional Redis (caching and resumable streams), and Alembic migrations.
PostgreSQL
The backend uses PostgreSQL as the only durable store for application data. All access is async via SQLAlchemy with the asyncpg driver.
Engine and session
backend/app/core/database.py:- Builds the async engine from
settings.POSTGRES_URLwithpool_pre_ping=Trueandpool_recycle=300so that connections are checked and recycled and do not outlive server-side idle timeouts. - Defines
AsyncSessionLocal(async_sessionmaker) withexpire_on_commit=Falseso that committed objects remain usable in the same request. - Exposes
get_db(): an async generator that yields anAsyncSessionand closes it in afinallyblock. Used as a FastAPI dependency so that each request gets a session and rollback is performed on exception.
Models (SQLAlchemy)
Models live in backend/app/models/. They use the declarative Base from core.database and UUID primary keys (PostgreSQL UUID type). Naming uses camelCase for application-facing fields (e.g. userId, createdAt).
| Model | Table | Purpose |
|---|---|---|
| User | User |
User account: email, password (nullable for guest/MSAL), type (guest/regular), name, azure_oid, password_changed_at. |
| Chat | Chat |
Conversation: id, createdAt, title, userId, visibility, lastContext (JSONB for usage etc.). |
| Message | (message table) | Single message in a chat: role, content/parts, association to chat. |
| Vote | (vote table) | Message vote (e.g. thumbs up/down). |
| Document | (document table) | User-created document. |
| File | (file table) | Uploaded file metadata. |
| Chart | (chart table) | Chart record (e.g. reference to MCP/chart spec). |
| Stream | (stream table) | Stream metadata for resumable streams. |
| Suggestion | (suggestion table) | Chat suggestions. |
| AuthSession | (auth session table) | Opaque session token (guest or user) with expiry. |
| RevokedToken | (revoked token table) | JWT JTI blacklist (logout, password change). |
| LoginAttempt | (login attempt table) | Login attempt tracking. |
| PasswordResetToken | (password reset table) | Password reset token. |
| PasswordResetAttempt | (password reset attempt table) | Reset attempt tracking. |
| AppFeedback | (app feedback table) | User feedback. |
Relationships (e.g. User.chats, Chat.messages) are defined so that the ORM can load related entities when needed. The query layer (see below) is the preferred way to read/write; handlers and AI code should not bypass it for consistency and testability.
Query layer
All database reads and writes go through backend/app/db/queries/. Each module encapsulates operations for one or more models and exposes async functions that take db: AsyncSession (injected via get_db()).
| Module | Domain |
|---|---|
chat_queries.py |
Create/get/update/delete chats; list messages; visibility. |
user_queries.py |
Get user by id, email, azure_oid; create user; update (e.g. password_changed_at). |
session_queries.py |
Create/validate/delete auth sessions (opaque tokens). |
document_queries.py |
Document CRUD. |
vote_queries.py |
Get/patch votes for messages. |
chart_queries.py |
Chart create/get. |
suggestion_queries.py |
Suggestions. |
revoked_token_queries.py |
Add JTI to revoked list; check if JTI is revoked. |
password_reset_queries.py |
Password reset token create/consume. |
password_reset_attempt_queries.py |
Reset attempt tracking. |
login_attempt_queries.py |
Login attempt tracking. |
Feedback and file operations may be in one of these modules or in the API layer; the principle is that persistence is centralized in the query layer so that business logic does not scatter raw SQL.
Redis (optional)
When REDIS_URL is set, the backend uses Redis for:
- Caching — For example, resolved user (by id or token) can be cached for
USER_CACHE_TTL_SECONDSto reduce DB round-trips on every request. - Resumable streams — When a user disconnects during a long LLM response, the backend can continue writing stream chunks to Redis keyed by stream id and sequence. When the client reconnects (e.g. with the same stream id), it can request the remaining chunks and the backend reads from Redis and streams them. This avoids re-running the full generation.
The Redis client is initialized on first use and closed in the FastAPI lifespan (in main.py) so that connections are released on shutdown. If REDIS_URL is empty, Redis-dependent features (e.g. resumable stream storage) are disabled.
Migrations (Alembic)
- Location:
backend/alembic/(standard Alembic layout:env.py,script.py.mako,versions/). - Usage: Migrations are run against the database using the sync or async URL (configurable; often
ALEMBIC_POSTGRES_URLor a sync equivalent). The application user (used at runtime) may need explicit GRANTs on tables and sequences; some migrations usedb/migration_utils.pyto grant privileges to the app user so that runtime connections can read/write.
New schema changes should be done via new migration scripts rather than changing models in place without a migration, so that existing deployments can upgrade cleanly.
Summary
- PostgreSQL is the single source of truth for users, chats, messages, votes, documents, files, charts, sessions, revoked tokens, feedback, and related entities. Access is async through the query layer in
app/db/queries/. - Redis is optional and used for caching and resumable stream chunks; the client is closed on app shutdown.
- Alembic manages schema versions; migrations may include grant steps for the app user.
The next document, Authentication, describes how users are identified and how auth interacts with the User and AuthSession models.