Database Component
Musings: On ORMs and Alternatives (September 2025)
I'm well aware of the... disdain... people have for ORMs. SQLModel is the default because it works great for 80% of use cases and integrates beautifully with FastAPI/Pydantic.
That said, I already have a sub-component feature in the works for folks who prefer raw SQL via driver cursors or SQLAlchemy Core. Choose your own adventure.
Database component with SQLModel ORM supporting SQLite and PostgreSQL. PostgreSQL can be self-hosted in a container or run on Neon serverless Postgres.
Use aegis init my-project --components database to include this component. Pick the engine (and, for PostgreSQL, the host) in the interactive prompt, or pass it non-interactively:
aegis init my-project --components "database[sqlite]" # file-based (default)
aegis init my-project --components "database[postgres]" # local postgres container
aegis init my-project --components "database[neon]" # Neon serverless Postgres
Neon is not a separate engine: database[neon] is the postgres engine hosted on Neon, so every model, migration, and query is identical to a self-hosted postgres stack.
Database Options
Choose your database based on your deployment needs:
File-based • Zero configuration • Development-friendly
Perfect for:
- Single-container deployments
- Development and testing
- Embedded applications
- Simple deployment requirements
Features:
- No external dependencies
- File-based simplicity
- Zero configuration
- Automatic directory creation
Limitations:
- Single-writer constraint
- Cross-container access challenges
- Limited concurrency
Server-based • Production-ready • High concurrency
Perfect for:
- Multi-container deployments
- Production environments
- High concurrency requirements
- Advanced database features
Features:
- Client-server architecture
- Connection pooling
- Multi-container support
- Advanced data types (JSONB, arrays)
Requirements:
- PostgreSQL service (via Docker Compose)
- Network configuration
- Credentials management
Serverless Postgres • No DB container in production • Cloud-hosted
# App runtime uses the POOLED endpoint (note the -pooler host)
DATABASE_URL=postgresql://user:pass@ep-xxxx-pooler.region.aws.neon.tech/dbname
# Migrations use the DIRECT (unpooled) endpoint
DATABASE_URL_UNPOOLED=postgresql://user:pass@ep-xxxx.region.aws.neon.tech/dbname
Perfect for:
- Serverless and autoscaling deployments
- Per-PR / preview database branches
- Teams that would rather not operate a database server
Neon is the postgres engine hosted on Neon's cloud: development still runs the local container, production connects to Neon. See the Neon page for endpoints, setup, and how the generated code adapts at runtime.
Choosing a Database
Cross-Container Considerations
SQLite limitation: File-based databases can't be shared across containers. If multiple services need database access (e.g., API + Scheduler), you need PostgreSQL.
PostgreSQL advantage: Network-based access allows any container to connect, making it ideal for microservice architectures.
Hosting PostgreSQL: container or Neon
Both database[postgres] and database[neon] use the same engine; they differ only in where production runs:
- Container (
database[postgres]): apostgres:16service runs in your Docker Compose stack in every environment (make serve,make serve-prod, andaegis deploy). You operate and back up the database. - Neon (
database[neon]): development still runs the local container, but production has no database service, the app connects to Neon's cloud viaDATABASE_URL, and Neon handles autoscaling, scale-to-zero, and branching.
The generated connection code detects a Neon host (*.neon.tech) at runtime and applies Neon's pooler-safe connection settings only then, so one codebase runs against the local container in development and Neon in production. See the Neon page for the full provider guide.
Common Patterns
Adding Your First Model
Step 1: Create Your Model
# app/models/shield.py
from sqlmodel import SQLModel, Field
class Shield(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
defense_rating: int = Field(default=10)
material: str = "iron"
enchantment: str | None = None
Step 2: Use It
# In your API endpoint
from app.core.db import db_session
from app.models.shield import Shield
@router.post("/shields")
async def forge_shield(shield_data: ShieldCreate):
with db_session() as session:
shield = Shield(**shield_data.dict())
session.add(shield)
# Automatically committed
return {"id": shield.id, "name": shield.name}
That's it! The database handles persistence automatically.
Session Management
from app.core.db import db_session
# Auto-commit on success, rollback on error
with db_session() as session:
shield = Shield(name="Dragon Scale Shield")
session.add(shield)
# Automatically committed when context exits
Migrations
Database schema changes are managed with Alembic:
Generate Migration
Apply Migrations
Migration Best Practices
- Generate migrations after model changes
- Review auto-generated migrations before applying
- Test migrations in development first
- Commit migrations to version control
- Migrations work identically for SQLite and PostgreSQL
- On Neon, migrations run over the direct (unpooled)
DATABASE_URL_UNPOOLEDendpoint automatically; the pooled endpoint is reserved for app runtime
Testing
The component includes test fixtures for database testing:
# tests/conftest.py provides:
@pytest.fixture
def db_session():
# Provides isolated test database session
# Automatically rolls back after each test
Test Infrastructure:
- In-memory test databases
- Transaction rollback patterns
- Isolated test environments
- Same patterns work for both SQLite and PostgreSQL
Next Steps
- SQLModel Documentation - Complete ORM capabilities
- Component Overview - Understanding Aegis Stack's component architecture