# Plan: Switch API persistence from InMemory to Postgres (TypeORM), keep InMemory for tests

Timestamp: 2025-12-28T20:17:49Z  
Scope: **Racing bounded context first**, dev switchable between InMemory and Postgres, tests keep forcing InMemory.

## Goals
- Make it **easy to switch** API persistence in dev via [`getApiPersistence()`](apps/api/src/env.ts:33) + [`process.env.GRIDPILOT_API_PERSISTENCE`](apps/api/src/env.d.ts:7).
- Default dev flow supports Postgres via Docker, but does **not** force it.
- Keep InMemory persistence intact and default for tests (already used in tests like [`process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory'`](apps/api/src/domain/bootstrap/BootstrapSeed.http.test.ts:17)).
- Implement Postgres/TypeORM persistence for **racing repositories** currently provided by [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:72).
- Provide **minimal idempotent seed** for Postgres so UI works (analogous to [`SeedRacingData`](apps/api/src/domain/bootstrap/BootstrapModule.ts:27)).

## Current state (what’s wired today)
- Env toggle exists: [`getApiPersistence()`](apps/api/src/env.ts:33).
- Postgres wiring exists but incomplete: [`TypeOrmModule.forRoot()`](apps/api/src/domain/database/DatabaseModule.ts:6) with `entities` commented and `synchronize` enabled outside production ([`synchronize`](apps/api/src/domain/database/DatabaseModule.ts:18)).
- Feature modules still hard-import in-memory racing persistence:
  - [`LeagueModule`](apps/api/src/domain/league/LeagueModule.ts:8)
  - [`RaceModule`](apps/api/src/domain/race/RaceModule.ts:8)
  - [`DashboardModule`](apps/api/src/domain/dashboard/DashboardModule.ts:2)
  - [`DriverModule`](apps/api/src/domain/driver/DriverModule.ts:2)
  - [`ProtestsModule`](apps/api/src/domain/protests/ProtestsModule.ts:2)
  - [`TeamModule`](apps/api/src/domain/team/TeamModule.ts:2)
  - [`SponsorModule`](apps/api/src/domain/sponsor/SponsorModule.ts:2)
  - [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:11)
- Dev compose currently forces InMemory even when `.env` provides `DATABASE_URL`:
  - Forced: [`GRIDPILOT_API_PERSISTENCE=inmemory`](docker-compose.dev.yml:36)
  - `.env` hints inference from `DATABASE_URL`: [`DATABASE_URL=postgres://...`](.env.development.example:20)

## High-level approach
1. Introduce a **persistence boundary module** for racing that selects implementation based on [`getApiPersistence()`](apps/api/src/env.ts:33).
2. Implement a Postgres/TypeORM module for racing repos (same tokens as in-memory).
3. Update racing-dependent API feature modules to import the boundary module (not the in-memory module).
4. Add minimal Postgres seed (dev-only, idempotent).
5. Fix dev compose to not hard-force InMemory.
6. Verify with lint/types/tests.

## Milestones (execution order)
### M1 — Create racing persistence boundary (switch point)
- Add [`RacingPersistenceModule`](apps/api/src/persistence/racing/RacingPersistenceModule.ts:1)
  - `imports`: choose one of:
    - [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:72)
    - New [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:1)
  - `exports`: re-export the chosen module’s tokens, so downstream modules remain unchanged.

Acceptance:
- No feature module directly imports [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:72) anymore.

### M2 — Add Postgres racing persistence module (skeleton)
- Add [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:1)
  - Provides **the same tokens** defined in [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:51):
    - [`LEAGUE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:52), [`RACE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:53), etc.
  - Uses `TypeOrmModule.forFeature([...entities])`.
  - Must be compatible with DB root config in [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:6).

Acceptance:
- API can start with `GRIDPILOT_API_PERSISTENCE=postgres` and resolve racing repository providers (even if repo methods are initially stubbed during iteration).

### M3 — Implement first working slice (League + Season + Membership + Race as needed)
- Implement ORM entities + mappers (ORM entities are not domain objects; follow clean architecture boundary from [`DOMAIN_OBJECTS.md`](docs/architecture/DOMAIN_OBJECTS.md:16)).
- Implement TypeORM repositories for the minimal feature set used by:
  - [`LeagueModule`](apps/api/src/domain/league/LeagueModule.ts:7)
  - [`RaceModule`](apps/api/src/domain/race/RaceModule.ts:7)

Strategy:
- Start from the endpoints exercised by existing HTTP tests that currently force InMemory (e.g. league schedule/roster tests), but run them in InMemory first; then add a small Postgres-specific smoke test later if needed.

Acceptance:
- Core use cases depending on racing repos function against Postgres in dev.

### M4 — Rewire feature modules to boundary module
Replace imports of [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:72) with [`RacingPersistenceModule`](apps/api/src/persistence/racing/RacingPersistenceModule.ts:1) in:
- [`LeagueModule`](apps/api/src/domain/league/LeagueModule.ts:1)
- [`RaceModule`](apps/api/src/domain/race/RaceModule.ts:1)
- [`DashboardModule`](apps/api/src/domain/dashboard/DashboardModule.ts:1)
- [`DriverModule`](apps/api/src/domain/driver/DriverModule.ts:1)
- [`ProtestsModule`](apps/api/src/domain/protests/ProtestsModule.ts:1)
- [`TeamModule`](apps/api/src/domain/team/TeamModule.ts:1)
- [`SponsorModule`](apps/api/src/domain/sponsor/SponsorModule.ts:1)
- plus adjust [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:10) (see M5).

Acceptance:
- Switching env var changes racing persistence without touching module imports.

### M5 — Minimal idempotent Postgres seed (dev UX)
- Extend bootstrap so Postgres mode can seed minimal data when DB is empty.
- Current bootstrap behavior only seeds racing data for InMemory: [`shouldSeedRacingData()`](apps/api/src/domain/bootstrap/BootstrapModule.ts:37).
- Update logic to also seed for Postgres when:
  - dev mode (non-prod), and
  - tables empty (e.g., count leagues/drivers), and
  - bootstrap enabled via [`getEnableBootstrap()`](apps/api/src/env.ts:49).

Implementation note:
- Seed code should remain adapter-level (reuse [`SeedRacingData`](apps/api/src/domain/bootstrap/BootstrapModule.ts:27)) but use repos from the active persistence module.

Acceptance:
- `docker compose -f docker-compose.dev.yml up` + `GRIDPILOT_API_PERSISTENCE=postgres` results in a usable UI without manual DB setup.

### M6 — Dev compose/env ergonomics
- Remove hard-coded forcing of InMemory in dev compose:
  - Change/remove [`GRIDPILOT_API_PERSISTENCE=inmemory`](docker-compose.dev.yml:36)
  - Prefer `.env.development` control, consistent with `.env example` guidance ([`DATABASE_URL`](.env.development.example:20)).

Acceptance:
- Devs can switch by editing `.env.development` or setting env override.

### M7 — Verification gate
Run:
- `eslint`
- `tsc`
- tests

Commands live in workspace scripts; start from package-level scripts as applicable (e.g. API tests via [`npm run test`](apps/api/package.json:10)).

Acceptance:
- No lint errors, no TypeScript errors, green tests (with default tests still using InMemory).

## Out of scope (this pass)
- Social persistence (stays InMemory for now).
- Full migration system (placeholder remains, e.g. [`up()`](adapters/persistence/migrations/001_initial_schema.ts:5)).
- Production-ready DB lifecycle (migrations, RLS, etc.).

## Risks / watchouts
- Provider scoping: racing repos are exported tokens; boundary module must avoid creating competing instances.
- Entity design: ORM entities must not leak into core (enforce boundary per [`DOMAIN_OBJECTS.md`](docs/architecture/DOMAIN_OBJECTS.md:16)).
- Bootstrap: ensure Postgres seed is idempotent and doesn’t run in production (align with `NODE_ENV` usage in [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:18)).