ca648ccf8b
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/dd261f53-4891-437e-bd71-5561ece62d7d
5.0 KiB
5.0 KiB
ADR 004: Use PostgreSQL as Primary Database
Status: Accepted
Date: 2026-01-28
Deciders: Development Team
Context
The application requires a relational database to store:
- User accounts and subscription data
- Mail account configurations (servers, protocols, check intervals)
- Processing run history and per-email logs
- Notification configurations
- Subscription plans and audit logs
- Database-backed application settings (
app_settingskey-value store)
Requirements:
- ACID transactions for financial/subscription data
- Foreign key constraints for referential integrity
- JSON support for flexible metadata storage
- Async driver support for FastAPI integration
- Horizontal read-scaling capability
Decision
We will use PostgreSQL 15+ as the primary relational database, accessed via SQLAlchemy 2.x with the asyncpg driver.
Alternatives Considered
1. MySQL / MariaDB
- Pros: Wide adoption, good tooling, familiar to many developers
- Cons: Historically weaker JSON support, slightly different SQL dialect, asyncio driver (aiomysql) less mature than asyncpg
2. SQLite
- Pros: Zero infrastructure, simple setup, file-based
- Cons: No concurrent writes, no horizontal scaling, not suitable for multi-user production SaaS
3. MongoDB
- Pros: Flexible schema, easy horizontal sharding, native JSON
- Cons: No ACID transactions across collections (before 4.0), weaker relational integrity, harder to query with joins, async support less mature
4. CockroachDB
- Pros: Distributed SQL, auto-sharding, highly available
- Cons: More complex deployment, higher cost, unnecessary for initial scale
Rationale
PostgreSQL was chosen because:
- ACID Compliance: Full transaction support critical for subscription billing and user data integrity
- JSON/JSONB Support: Native JSON columns allow flexible metadata without schema migrations
- asyncpg Driver: The fastest PostgreSQL async driver for Python, purpose-built for asyncio
- SQLAlchemy 2.x Async: Mature async ORM integration via
create_async_engineandAsyncSession - Extension Ecosystem: uuid-ossp, pgcrypto, and other extensions available if needed
- Alembic Migrations: SQLAlchemy's Alembic integrates seamlessly for schema version control
- Row-Level Security: Available for multi-tenant data isolation if required in future
- Industry Standard: Well-understood operational characteristics, strong community, excellent documentation
Implementation Details
Async Engine Configuration
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
engine = create_async_engine(
settings.DATABASE_URL,
echo=False,
connect_args={"prepared_statement_cache_size": 0}, # Avoids plan invalidation when create_all() runs CREATE TYPE DDL at startup
)
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
Session Dependency
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
yield session
Schema Management
# Auto-generate migration from model changes
alembic revision --autogenerate -m "add gmail_credentials table"
# Apply all pending migrations
alembic upgrade head
Consequences
Positive
- Full relational integrity with foreign keys and constraints
- Async I/O with asyncpg eliminates blocking database calls
- Alembic provides version-controlled, reviewable schema changes
- Familiar SQL tooling (pgAdmin, psql, etc.) for debugging
- Supports connection pooling (PgBouncer) for high-concurrency deployments
Negative
- Additional infrastructure to deploy and operate (unlike SQLite)
- asyncpg prepared statement cache must be disabled when
create_all()runsCREATE TYPE … AS ENUMDDL at startup — this DDL invalidates cached plans on the same connection, causing the next enum-type existence check to fail withProgrammingError: cached statement plan is invalid(fix:prepared_statement_cache_size=0) - Async SQLAlchemy patterns are more complex than synchronous ORM patterns
Neutral
- Redis is still required as a separate service (for Celery broker/result backend)
- Database backups must be configured separately (pg_dump or WAL archiving)
Migration Strategy
All schema changes are managed via Alembic:
- Development: auto-generate from SQLAlchemy model changes
- Production: migrations run explicitly via
alembic upgrade head - Rollback:
alembic downgrade -1for single-step rollback
Related Decisions
- See ADR-001 for Celery (uses Redis, separate from PostgreSQL)
- See ADR-010 for hybrid configuration model (uses
app_settingstable in PostgreSQL)