# Plan: Switch Racing persistence from InMemory to Postgres (TypeORM) while keeping InMemory for tests

Timestamp: 2025-12-28T20:46:00Z  
Scope: Racing bounded context persistence only (no Social/Identity/Media/Payments persistence changes)

This plan is intentionally implementation-ready (what files to add, what to wire, what tests to write first) while keeping scope controlled: a minimal vertical slice that makes one meaningful League/Race workflow work in dev Postgres, but does not attempt to implement every Racing repository at once.

---

## 0) Context (current state, must preserve)

- Persistence toggle is already defined at [`getApiPersistence()`](apps/api/src/env.ts:33) and typed in [`ProcessEnv`](apps/api/src/env.d.ts:3).
- DB bootstrap exists as Nest module: [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:1), with non-prod schema sync enabled via [`synchronize`](apps/api/src/domain/database/DatabaseModule.ts:18).
- Racing repository tokens are currently defined in [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:51) and are used as Nest provider tokens.
- Persistence boundary already exists and selects between in-memory and Postgres: [`RacingPersistenceModule`](apps/api/src/persistence/racing/RacingPersistenceModule.ts:1).
- Postgres wiring for Racing is currently placeholder-only: [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:51).
- Clean Architecture rules to enforce (no ORM leakage into Core):
  - [`DATA_FLOW.md`](docs/architecture/DATA_FLOW.md:1)
  - [`DOMAIN_OBJECTS.md`](docs/architecture/DOMAIN_OBJECTS.md:1)
  - File placement rules via [`FILE_STRUCTURE.md`](docs/architecture/FILE_STRUCTURE.md:1)

---

## 1) Goal and non-goals

### Goal
Enable Racing persistence via Postgres/TypeORM for development runtime (selected via [`getApiPersistence()`](apps/api/src/env.ts:33)), while keeping default test runs using in-memory persistence (to keep CI fast and deterministic).

### Non-goals (explicit scope control)
- No migration framework rollout, no production migration story beyond the existing non-prod `synchronize` behavior in [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:18).
- No broad refactors to use cases, DTOs, controllers, or domain modeling.
- No implementation of non-Racing bounded contexts (Social/Identity/Media/etc).
- No attempt to “finish all Racing repositories” in first pass; we will do a minimal vertical slice first.

---

## 2) Proposed adapter folder/file layout (TypeORM under `adapters/`)

This follows the existing adapter organization shown in [`FILE_STRUCTURE.md`](docs/architecture/FILE_STRUCTURE.md:41) and the repo’s existing Racing adapter grouping under `adapters/racing/persistence/inmemory`.

Create a parallel `typeorm` tree for Racing persistence:

- `adapters/racing/persistence/typeorm/README.md`
- `adapters/racing/persistence/typeorm/entities/`
  - `LeagueOrmEntity.ts`
  - `SeasonOrmEntity.ts`
  - `LeagueScoringConfigOrmEntity.ts`
  - `RaceOrmEntity.ts`
  - `LeagueMembershipOrmEntity.ts`
- `adapters/racing/persistence/typeorm/mappers/`
  - `LeagueOrmMapper.ts`
  - `SeasonOrmMapper.ts`
  - `LeagueScoringConfigOrmMapper.ts`
  - `RaceOrmMapper.ts`
  - `LeagueMembershipOrmMapper.ts`
- `adapters/racing/persistence/typeorm/repositories/`
  - `TypeOrmLeagueRepository.ts`
  - `TypeOrmSeasonRepository.ts`
  - `TypeOrmLeagueScoringConfigRepository.ts`
  - `TypeOrmRaceRepository.ts`
  - `TypeOrmLeagueMembershipRepository.ts`
- `adapters/racing/persistence/typeorm/testing/` (test-only helpers; never imported by prod code)
  - `createTypeOrmTestDataSource.ts`
  - `truncateRacingTables.ts`

Notes:
- Do not add any `index.ts` barrel files due to the lint restriction in [`.eslintrc.json`](.eslintrc.json:36).
- All mapping logic must live in adapters (never in Core), per [`DATA_FLOW.md`](docs/architecture/DATA_FLOW.md:24).

---

## 3) Minimal vertical slice (useful and controlled)

### 3.1 “Meaningful workflow” target
Implement the smallest set that supports this end-to-end workflow in Postgres dev:

1) Create a League (creates Season + scoring config) via the API call handled by the service method [`LeagueService.createLeague()`](apps/api/src/domain/league/LeagueService.ts:773) which uses [`CreateLeagueWithSeasonAndScoringUseCase`](core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts:56).  
2) Fetch that League’s schedule (or races list) via [`GetLeagueScheduleUseCase`](core/racing/application/use-cases/GetLeagueScheduleUseCase.ts:33), used by [`LeagueService.getLeagueSchedule()`](apps/api/src/domain/league/LeagueService.ts:614).

This is a practical vertical slice: it enables the admin UI to create a league and see a schedule scaffold.

### 3.2 Repos/tokens INCLUDED in slice 1
Implement Postgres/TypeORM repositories for the following tokens from [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:51):

- [`LEAGUE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:52) → `TypeOrmLeagueRepository`
- [`SEASON_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:62) → `TypeOrmSeasonRepository`
- [`LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:64) → `TypeOrmLeagueScoringConfigRepository`
- [`RACE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:53) → `TypeOrmRaceRepository`
- [`LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:56) → `TypeOrmLeagueMembershipRepository`

Rationale:
- Creation flow requires the first three via [`CreateLeagueWithSeasonAndScoringUseCase`](core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase.ts:56).
- Schedule flow requires `league`, `season`, `race` repos via [`GetLeagueScheduleUseCase`](core/racing/application/use-cases/GetLeagueScheduleUseCase.ts:33).
- Capacity listing and “join league” depend on membership repo via [`GetAllLeaguesWithCapacityUseCase`](core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase.ts:25) and [`JoinLeagueUseCase`](core/racing/application/use-cases/JoinLeagueUseCase.ts:18).

### 3.3 Repos/tokens DEFERRED (slice 2+)
Explicitly defer these tokens from [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:51) to later phases:

- [`DRIVER_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:51)
- [`RESULT_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:54)
- [`STANDING_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:55)
- [`RACE_REGISTRATION_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:57)
- [`TEAM_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:58)
- [`TEAM_MEMBERSHIP_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:59)
- Anything sponsorship/wallet-related in Racing persistence (tokens near [`LEAGUE_WALLET_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:66))

This keeps slice 1 focused and prevents exploding schema surface area.

---

## 4) Mapping strategy (ORM entities vs domain entities)

### 4.1 Boundary rule (non-negotiable)
Core domain entities (example [`League`](core/racing/domain/entities/League.ts:93), [`Race`](core/racing/domain/entities/Race.ts:18)) MUST NOT import or refer to ORM entities, repositories, decorators, or TypeORM types, per [`DOMAIN_OBJECTS.md`](docs/architecture/DOMAIN_OBJECTS.md:16) and [`DATA_FLOW.md`](docs/architecture/DATA_FLOW.md:24).

### 4.2 ORM entity design principles
- ORM entities are persistence models optimized for storage/querying and can use primitives (string, number, Date) and JSON columns.
- Domain entities use rich value objects and invariants and should be built using existing factories like [`League.create()`](core/racing/domain/entities/League.ts:132) and [`Race.create()`](core/racing/domain/entities/Race.ts:81).

### 4.3 Mapping responsibilities
Mapping lives in `adapters/racing/persistence/typeorm/mappers/*` and is responsible for:

- `toDomain(orm)`:
  - Convert primitive columns/JSON back into domain props.
  - Call domain factory methods (`create`) with validated values.
  - Handle optional fields and backward-compatibility defaults (e.g., `League.settings` in [`League`](core/racing/domain/entities/League.ts:72)).
- `toOrm(domain)`:
  - Convert domain value objects to primitives suitable for columns.
  - Define canonical serialization for nested structures (e.g., store `League.settings` as JSONB).

### 4.4 Proposed per-entity mapping notes (slice 1)

#### League
- Persist fields:
  - `id` string (PK)
  - `name` string
  - `description` string
  - `ownerId` string
  - `createdAt` Date
  - `settings` JSONB (store `LeagueSettings` from [`LeagueSettings`](core/racing/domain/entities/League.ts:72))
  - `socialLinks` JSONB nullable
  - `participantCount` integer (if needed; domain tracks via internal `_participantCount` in [`League`](core/racing/domain/entities/League.ts:103))
  - `visibility` string (redundant to settings.visibility, but may be useful for querying; keep either:
    - Option A: derive from settings only and do not store separate column
    - Option B: store both and enforce consistency in mapper (preferred for query ergonomics)

#### Season
- Keep Season as its own ORM entity with FK to leagueId (string).
- Use JSONB for schedule (if schedule is a complex object), and scalar columns for status, year, order, start/end.

#### LeagueScoringConfig
- Store `seasonId` string as unique FK.
- Store scoring config payload (championships, points tables, bonus rules) as JSONB.

#### Race
- Persist scalar fields corresponding to [`Race`](core/racing/domain/entities/Race.ts:18):
  - `id` string (PK)
  - `leagueId` string (indexed)
  - `scheduledAt` timestamptz
  - `track`, `trackId`, `car`, `carId` strings
  - `sessionType` string
  - `status` string (from [`RaceStatus`](core/racing/domain/entities/Race.ts:11))
  - `strengthOfField`, `registeredCount`, `maxParticipants` integers nullable
- Queries required by Core ports (examples in [`IRaceRepository`](core/racing/domain/repositories/IRaceRepository.ts:10)):
  - find by leagueId
  - upcoming/completed filtering (status + scheduledAt)

#### LeagueMembership
- Persist fields corresponding to [`LeagueMembership`](core/racing/domain/entities/LeagueMembership.ts:25):
  - `id` string (domain uses default `${leagueId}:${driverId}` in [`LeagueMembership.create()`](core/racing/domain/entities/LeagueMembership.ts:49))
  - `leagueId` string (indexed)
  - `driverId` string (indexed)
  - `role` string
  - `status` string
  - `joinedAt` timestamptz
- This enables membership queries required by [`ILeagueMembershipRepository`](core/racing/domain/repositories/ILeagueMembershipRepository.ts:13).

---

## 5) TypeORM + Nest wiring specifics

### 5.1 Database root config
Current `DatabaseModule` uses [`TypeOrmModule.forRoot()`](apps/api/src/domain/database/DatabaseModule.ts:6) and does not register entities.

Plan change (minimal and controlled):
- Update [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:1) to support loading Racing entities when Postgres persistence is enabled:
  - Add `autoLoadEntities: true` in the `forRoot` options so entities registered via feature modules are discovered.
  - Keep [`synchronize`](apps/api/src/domain/database/DatabaseModule.ts:18) behavior as-is for non-prod for now (explicitly acknowledged technical debt).

Why:
- We want Racing persistence to be modular (entities registered only when the Postgres Racing module is imported) without a global “list every entity in the world” change.

### 5.2 Postgres Racing module structure
Replace placeholder providers in [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:51) with real wiring:

- `imports`:
  - [`LoggingModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:3) (already present)
  - Nest TypeORM feature registration for the slice 1 entities:
    - `TypeOrmModule.forFeature([LeagueOrmEntity, SeasonOrmEntity, LeagueScoringConfigOrmEntity, RaceOrmEntity, LeagueMembershipOrmEntity])`
      - Mentioned as a method name; `TypeOrmModule` itself is already in use at [`TypeOrmModule.forRoot()`](apps/api/src/domain/database/DatabaseModule.ts:6).

- `providers`:
  - Register each repository implementation class under the existing tokens, matching the in-memory token names exactly (examples in [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:75)):
    - Provide [`LEAGUE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:52) → `TypeOrmLeagueRepository`
    - Provide [`SEASON_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:62) → `TypeOrmSeasonRepository`
    - Provide [`LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:64) → `TypeOrmLeagueScoringConfigRepository`
    - Provide [`RACE_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:53) → `TypeOrmRaceRepository`
    - Provide [`LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:56) → `TypeOrmLeagueMembershipRepository`

- `exports`:
  - Export the same tokens so downstream modules remain unchanged, mirroring the export list pattern in [`InMemoryRacingPersistenceModule`](apps/api/src/persistence/inmemory/InMemoryRacingPersistenceModule.ts:182).

### 5.3 Repository implementation style
Each `TypeOrm*Repository`:
- Implements the relevant Core repository interface, e.g. [`ILeagueRepository`](core/racing/domain/repositories/ILeagueRepository.ts:10).
- Depends only on:
  - TypeORM repository/DataSource types (adapter-layer OK)
  - Mappers in adapters
  - Domain entities/ports (core-layer OK)
- Does not expose ORM entities outside adapters.

### 5.4 Persistence boundary selection remains the same
Do not change selection semantics in [`RacingPersistenceModule`](apps/api/src/persistence/racing/RacingPersistenceModule.ts:7). This module already selects Postgres vs in-memory using [`getApiPersistence()`](apps/api/src/env.ts:33).

---

## 6) TDD-first phased rollout (tests first, controlled scope)

### 6.1 Testing goals
- Add confidence that TypeORM repositories satisfy Core port contracts.
- Keep default test runs fast and in-memory by default.
- Add Postgres-backed integration tests that are opt-in (run only when explicitly enabled).

### 6.2 What tests already exist and should remain green
- Persistence module selection test: [`RacingPersistenceModule.test.ts`](apps/api/src/persistence/racing/RacingPersistenceModule.test.ts:9) (currently asserts placeholder instance in Postgres mode). This will need updating once placeholders are replaced, but the intent remains valid.
- Existing in-memory repository unit tests under `adapters/racing/persistence/inmemory/*.test.ts` (example: [`InMemoryLeagueRepository.test.ts`](adapters/racing/persistence/inmemory/InMemoryLeagueRepository.test.ts)) must remain untouched and continue to run by default.

### 6.3 New tests to write first (TDD sequence)

#### Phase A: Contract-style repo tests for TypeORM (integration tests, opt-in)
Create a new test suite for each TypeORM repository in:
- `adapters/racing/persistence/typeorm/repositories/*Repository.integration.test.ts`

Test approach:
- Use a real Postgres database (not mocks) and TypeORM DataSource configured similarly to runtime config in [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:6).
- Keep these tests skipped unless a flag is set, e.g. `RUN_RACING_PG_TESTS=1` (exact naming to be decided in implementation mode).
- Use a dedicated DB name or schema per run and truncate tables between tests.

Example test cases (for slice 1):
- `TypeOrmLeagueRepository` should satisfy basic operations defined in [`ILeagueRepository`](core/racing/domain/repositories/ILeagueRepository.ts:10):
  - create + findById roundtrip
  - findAll returns inserted
  - update roundtrip
  - exists works
- `TypeOrmSeasonRepository` should satisfy [`ISeasonRepository`](core/racing/domain/repositories/ISeasonRepository.ts:3):
  - create + findById
  - findByLeagueId
- `TypeOrmRaceRepository` should satisfy [`IRaceRepository`](core/racing/domain/repositories/IRaceRepository.ts:10):
  - create + findById
  - findByLeagueId
  - findUpcomingByLeagueId and findCompletedByLeagueId behavior (status + date)
- `TypeOrmLeagueMembershipRepository` should satisfy [`ILeagueMembershipRepository`](core/racing/domain/repositories/ILeagueMembershipRepository.ts:13):
  - saveMembership + getMembership
  - getLeagueMembers filtering (active vs pending) must match whatever domain expects (start with minimal “returns all stored” behavior, then align with use cases)
- `TypeOrmLeagueScoringConfigRepository` should satisfy [`ILeagueScoringConfigRepository`](core/racing/domain/repositories/ILeagueScoringConfigRepository.ts:3):
  - save + findBySeasonId

Why integration tests first:
- It forces us to design the ORM schema + mapping in a way that matches the Core port contracts immediately.

#### Phase B: Update module selection test (unit test, always-on)
Update [`RacingPersistenceModule.test.ts`](apps/api/src/persistence/racing/RacingPersistenceModule.test.ts:9) to assert that in Postgres mode the provider resolves to the real TypeORM repo class (instead of the placeholder in [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:30)).

This remains a fast unit test: it only checks Nest DI wiring, not DB behavior.

### 6.4 What to mock vs run “for real”
- Mock nothing for repository integration tests (they should hit Postgres).
- Keep Core use case tests (if any exist) running with in-memory repos or test doubles by default.
- Do not switch existing HTTP tests to Postgres by default (many explicitly set in-memory via env in files like those discovered under `apps/api/src/domain/league/*.http.test.ts` via earlier repo search).

### 6.5 Keeping default tests in in-memory mode
- Preserve the current default behavior where tests set [`GRIDPILOT_API_PERSISTENCE`](apps/api/src/env.d.ts:7) to `inmemory` (example in [`RacingPersistenceModule.test.ts`](apps/api/src/persistence/racing/RacingPersistenceModule.test.ts:20)).
- Ensure the Postgres integration tests are opt-in and not included in default `npm run api:test` from [`apps/api/package.json`](apps/api/package.json:10).

---

## 7) Dev bootstrap/seed strategy for Postgres (minimal, idempotent, non-test)

### 7.1 Current behavior
Bootstrap currently seeds racing data only in in-memory mode via [`shouldSeedRacingData()`](apps/api/src/domain/bootstrap/BootstrapModule.ts:37), which returns `true` only for `inmemory`.

### 7.2 Target behavior
In dev Postgres mode, seed minimal Racing data only when the database is empty, and never during tests.

Proposed logic change in [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:10):
- Seed when ALL are true:
  - `NODE_ENV !== 'production'`
  - persistence is Postgres via [`getApiPersistence()`](apps/api/src/env.ts:33)
  - database appears empty for Racing (fast check: `driverRepository.findAll().length === 0` as already used in [`SeedRacingData.execute()`](adapters/bootstrap/SeedRacingData.ts:55))
  - bootstrap is enabled (already toggled globally in app startup via [`getEnableBootstrap()`](apps/api/src/env.ts:49) and [`AppModule`](apps/api/src/app.module.ts:29))

Implementation detail:
- Keep using [`SeedRacingData`](adapters/bootstrap/SeedRacingData.ts:49) because it is already idempotent-ish (it skips when drivers exist at [`SeedRacingData.execute()`](adapters/bootstrap/SeedRacingData.ts:55)).
- Ensure Postgres-backed `driverRepository` is not required for slice 1 if we keep seed minimal; however, current seed checks drivers first, so this implies either:
  - Option A (preferred for UI usefulness): include Driver repo in Postgres slice 1b (small extension) so seeding can run fully, or
  - Option B (controlled scope): create a Postgres-only “minimal seed” class that checks `leagueRepository.findAll()` instead of drivers and seeds only leagues/seasons/races/memberships/scoring configs.

To keep scope controlled and aligned with “Racing only”, choose Option B for slice 1:
- Introduce `SeedRacingDataMinimal` under `adapters/bootstrap/racing/` that seeds:
  - one league
  - one active season
  - 0..N races in the season window
  - one membership for the owner (so capacity endpoints have meaningful data)
  - one scoring config for the active season
- Keep it idempotent:
  - skip if `leagueRepository.findAll()` returns any leagues
  - upsert behavior for scoring config by seasonId (align with [`ILeagueScoringConfigRepository.findBySeasonId()`](core/racing/domain/repositories/ILeagueScoringConfigRepository.ts:3))

Test contamination avoidance:
- Tests already default to in-memory persistence and can also set `GRIDPILOT_API_BOOTSTRAP` to false if needed via [`getEnableBootstrap()`](apps/api/src/env.ts:49).

---

## 8) Dev ergonomics: docker-compose dev toggle change (exact change)

### 8.1 Current issue
`docker-compose.dev.yml` forces in-memory persistence via [`GRIDPILOT_API_PERSISTENCE=inmemory`](docker-compose.dev.yml:36), which overrides the inference behavior in [`getApiPersistence()`](apps/api/src/env.ts:33).

### 8.2 Proposed exact change (minimal)
Update [`docker-compose.dev.yml`](docker-compose.dev.yml:26) to stop hard-forcing in-memory:

- Replace the hard-coded line at [`docker-compose.dev.yml`](docker-compose.dev.yml:36) with:
  - `- GRIDPILOT_API_PERSISTENCE=${GRIDPILOT_API_PERSISTENCE:-postgres}`

Expected dev behavior:
- Default dev stack uses Postgres persistence (because compose default becomes `postgres`).
- Developers can still run in-memory explicitly by setting `GRIDPILOT_API_PERSISTENCE=inmemory` before running the compose command.

Alternative (if you prefer to preserve auto-detection):
- Remove the line entirely and rely on [`getApiPersistence()`](apps/api/src/env.ts:33) + `DATABASE_URL` presence in `.env.development`.

This plan recommends the explicit compose default approach because it is more deterministic and avoids hidden coupling to `.env.development` contents.

---

## 9) Phased implementation plan (step-by-step)

### Phase 1: Prepare TypeORM adapter skeleton (no behavior change)
1) Add the folder structure described in section 2.
2) Add the ORM entity files for the slice 1 domain models with minimal columns and constraints (PKs, required columns, basic indices).
3) Add mapper stubs with round-trip intent documented.
4) Add repository class stubs that implement the Core interfaces but throw “not implemented” only for methods not used in slice 1 tests.

Gate:
- No changes to runtime wiring yet; existing tests remain green.

### Phase 2: Add opt-in Postgres integration tests (TDD)
1) Add TypeORM DataSource test helper.
2) Write failing integration tests for `TypeOrmLeagueRepository` and implement it until green.
3) Repeat for `Season`, `Race`, `Membership`, `ScoringConfig` repos.

Gate:
- Integration tests pass when enabled.
- Default `npm run api:test` remains unaffected.

### Phase 3: Wire Postgres Racing module to real repos (DI correctness)
1) Update [`PostgresRacingPersistenceModule`](apps/api/src/persistence/postgres/PostgresRacingPersistenceModule.ts:51):
   - Import TypeORM feature registration (section 5.2).
   - Replace placeholder providers with real repository providers for slice 1 tokens.
2) Update [`RacingPersistenceModule.test.ts`](apps/api/src/persistence/racing/RacingPersistenceModule.test.ts:9) to assert the Postgres providers resolve to the real TypeORM repo classes (instead of placeholders).

Gate:
- Always-on module selection tests pass.

### Phase 4: Enable dev Postgres UX (bootstrap + compose)
1) Update [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:10) to seed minimal Racing data in dev Postgres mode only when empty.
2) Update [`docker-compose.dev.yml`](docker-compose.dev.yml:26) per section 8.

Gate:
- `docker compose` dev stack can run with Postgres persistence and UI has minimal data.

### Phase 5: Incrementally expand beyond slice 1 (future, explicitly not required to finish now)
Add Driver + Result + Standings etc only when a concrete UI/endpoint requires them and after writing the next integration tests first.

---

## 10) Verification gates (exact commands and when)

Run these at the end of each phase that changes TS code:

- ESLint:
  - `npm run lint` (script defined at [`package.json`](package.json:80))
- TypeScript:
  - `npm run typecheck:targets` (script defined at [`package.json`](package.json:120))
- API tests:
  - `npm run api:test` (script defined at [`package.json`](package.json:64)) or `npm run test --workspace=@gridpilot/api` via [`apps/api/package.json`](apps/api/package.json:10)

For opt-in Postgres repository integration tests (added in Phase 2):
- Define a dedicated command (implementation-mode decision), but the plan expects it to be an explicit command that developers run intentionally (not part of default CI).

---

## 11) Risks and mitigations

- Risk: ORM entities leak into Core through shared types.
  - Mitigation: enforce mappers in adapters only, keep interfaces as Core ports (example [`ILeagueRepository`](core/racing/domain/repositories/ILeagueRepository.ts:10)).
- Risk: Seed logic contaminates tests.
  - Mitigation: preserve default in-memory persistence in tests (example env usage in [`RacingPersistenceModule.test.ts`](apps/api/src/persistence/racing/RacingPersistenceModule.test.ts:20)) and gate seeding by non-prod + emptiness checks; tests can disable bootstrap via [`getEnableBootstrap()`](apps/api/src/env.ts:49).
- Risk: TypeORM entity registration not picked up because entities not configured.
  - Mitigation: enable `autoLoadEntities` in [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:1) as part of implementation.

---

## 12) Ready-for-approval questions (for implementation mode to resolve quickly)

These are the only decisions that materially affect implementation detail:

1) Prefer Postgres integration tests using a developer-managed Postgres (via `DATABASE_URL`) or a dedicated docker-compose test database?
2) For slice 1 seed, should we implement a minimal Racing-only seed (recommended) or extend slice 1 to include Driver repo so we can reuse [`SeedRacingData`](adapters/bootstrap/SeedRacingData.ts:49) unchanged?