Date: 2026-05-02
Accepted — supersedes ADR-0003
The project used SQLite as its only database engine (ADR-0003). This meant all environments — local development, Docker Compose, and any deployment — ran the same SQLite file-based database. For the majority of contributors this is ideal: zero infrastructure required.
However, a fixed SQLite-only setup has limits:
- SQLite does not support concurrent writes, so multi-instance deployments are not possible.
- Developers who want to validate production-parity behaviour (e.g. PostgreSQL-specific query plans, type semantics, or constraint handling) have no path to do so without modifying the project.
- A fixed SQLite-in-dev / PostgreSQL-in-prod split (a common alternative) introduces a different problem: subtle behavioral differences between environments that are hard to catch locally.
The goal is to make the database engine fully configurable with a single environment variable, so the same stack is used consistently across every environment a developer chooses to run.
We will introduce a DATABASE_PROVIDER environment variable that selects the database engine at startup:
DATABASE_PROVIDER=sqlite(default): SQLite everywhere. Zero infrastructure required. Works on any machine without Docker. Clone and run.DATABASE_PROVIDER=postgres: PostgreSQL everywhere. Requires Docker. Opt-in for developers who want a server-based engine or full production parity.
The default is sqlite to keep the barrier to entry as low as possible.
ServiceCollectionExtensions.AddDbContextPool reads DATABASE_PROVIDER and wires the appropriate EF Core provider:
switch (provider.ToLowerInvariant())
{
case "postgres":
options.UseNpgsql(Environment.GetEnvironmentVariable("DATABASE_URL"));
break;
default:
options.UseSqlite($"Data Source={dataSource}");
break;
}SQLite and PostgreSQL require different column type mappings (TEXT/INTEGER vs uuid/boolean/timestamp with time zone). A single migration set cannot satisfy both providers. To resolve this:
- SQLite migrations remain in
Migrations/(namespace...Migrations). - PostgreSQL migrations are placed in
Migrations/Npgsql/(namespace...Migrations.Npgsql). - A custom
ProviderSpecificMigrationsAssembly(implementing EF Core'sIMigrationsAssembly) filters the discovered migrations to only those in the active provider's namespace. It is registered viaoptions.ReplaceService<IMigrationsAssembly, ProviderSpecificMigrationsAssembly>().
MigrateAsync() at startup continues to work for both providers; each sees only its own migration set.
The postgres service is declared under the postgres Compose profile so it only starts when explicitly requested:
# SQLite (default — no extra Docker service)
docker compose up
# PostgreSQL (opt-in)
DATABASE_PROVIDER=postgres docker compose --profile postgres up- Developers choose their engine once and use it consistently across all environments.
- SQLite remains the zero-friction default — clone, run, done.
- PostgreSQL is a first-class option for production-parity testing without requiring project-wide changes.
- The Compose profile approach prevents accidental PostgreSQL containers from starting in SQLite mode.
dotnet runcontinues to work unchanged with SQLite.
- Two parallel migration sets must be maintained. Adding a schema change requires migrations in both
Migrations/andMigrations/Npgsql/. ProviderSpecificMigrationsAssemblyuses an EF Core internal API (MigrationsAssemblyfromMicrosoft.EntityFrameworkCore.Migrations.Internal), annotated with#pragma warning disable EF1001. This may require updates on major EF Core version upgrades.- PostgreSQL support is not tested in CI (no Testcontainers integration yet — tracked in issue #353).
- The
DATABASE_URLconnection string format follows the Npgsql convention (Host=...;Database=...;Username=...;Password=...). .env.exampledocuments all three new environment variables (DATABASE_PROVIDER,DATABASE_URL,POSTGRES_PASSWORD)..envis git-ignored.