Recommended order Wave 1 — pilots (establish the template on easy wins) 1. tagService (26 lines, zero deps, only consumers: TagsModule + 1 MCP tool) — the ideal first migration. Small enough that the whole pattern fits in one PR: move the SQL into TagsService with injected DatabaseService, retire the legacy file, convert its tests to overrideProvider(DATABASE_CONNECTION). 2. categoryService (31 lines, same shape) — immediate repeat to confirm the template. 3. todoService (120 lines, classic CRUD with transactions + broadcast) — first "full-shaped" domain: prepared statements, db.transaction, WebSocket broadcast, 3 MCP tools. After this one, the recipe is settled for everything else. The pilots deliberately include MCP consumers, because they force the one open design decision early: MCP tools run outside the Nest container, so either the migrated module keeps thin function exports delegating to a shared instance, or the MCP layer gets a handle to the Nest app (app.get(TodoService)). Better to settle that on a 26-line service than on authService. Wave 2 — cross-cutting leverage (parallel to wave 3, high payoff) 4. permissions (149 lines, zero deps, 17 Nest consumers) → an injectable PermissionsService (done 2026-07 — every module wrapper, the airtrail-import controller and PluginHostDepsFactory now inject it; the cache stays module-scoped so the DI singleton and permissions.bridge — kept for mcp/_shared.ts + adminService/authService/backupService/collectionsService — share one invalidation). Every module wrapper already calls checkPermission; this converts the single most-imported seam in one move. 5. auditLog (171 lines, 13 Nest consumers) → AuditService (done 2026-07 — writeAudit is the injectable; getClientIp and the log*/LOG_LEVEL logger stay plain modules in nest/audit/ (client-ip.ts, audit-log.logger.ts with its frozen-at-import level + mkdir as a documented parity exception); audit.bridge covers mcp/index, oauthProvider and the legacy airtrail/immich/oauth services). 6. tripAccess (9 lines) — don't migrate, delete: it's a wrapper around canAccessTrip, which DatabaseService now exposes. Absorb its 11 call sites opportunistically. Wave 3 — domain services with existing modules, low fan-in 7. dayNoteService (done 2026-07), tripInviteService (done 2026-07), assignmentService (done 2026-07), shareService (done 2026-07), settingsService (done 2026-07), fileService (done 2026-07), collabService (done 2026-07), packingService (done 2026-07) — each maps 1:1 to a Nest module that's already a thin delegate; none is imported by more than one other legacy service. Wave 4 — the coupled cluster (order matters here) 8. dayService → budgetService → reservationService in that order: reservations depend on both (budget-item linking, day resolution), so migrating the dependencies first lets ReservationsService inject the finished versions instead of importing legacy functions. — **Correction (migration-graph.md, borne out by the migration): the claimed ordering constraint doesn't exist at the service layer.** reservationService (done 2026-07) imported neither budgetService nor dayService; the budget/day coupling lives in the Nest wrapper's budget-sync seam and the MCP registrars, which keep their legacy imports until those domains migrate. Reservations went first as the frontier residue fold; dayService (done 2026-07 — the 592-line service folded into the wrapper `DaysService`, the accommodations seam in `nest/reservations/` now injects it, and the hand-rolled reorder/insert transactions became `db.transaction()`) followed; budgetService followed (done 2026-07 — the 755-line money core folded into the wrapper `BudgetService` (settlement maths, FX freeze/rebase, items/members/payers CRUD; the raw settlement writes became `insertSettlement`/`applySettlementUpdate` so the MCP paths keep skipping the freeze); the 11-tool mcp registrar + 3 budget resources moved to the decorator-driven `budget.mcp.ts`; TripsService/ReservationsService/BookingImportService now inject `BudgetService`; a 4-export `budget.bridge.ts` serves tripService, userCleanupService and the legacy trips/transports registrars; `exchange-rates.bridge.ts` was deleted with its last consumers; and the controller adopted `budget.dto.ts` over the shared schemas, clearing all nine budget allow-list entries — **tripService is now unblocked**). 9. tripService (done 2026-07 — the 1121-line hub folded into the wrapper `TripsService`: generateDays, the updateTrip date-shift transaction, members/guests (#973/#1362), the ICS export, copyTripById and getTripSummary; its six bridge imports became injected services (CollabService/VacayService joined the constructor); the 10-tool trips registrar + 3 trip resources + the trip-summary prompt moved to the decorator-driven `trips.mcp.ts` (first `@Prompt`; the static-token deprecation notice rides the `registry.attach` ctx) and the 3 share-link tools to `share.mcp.ts` on the `canShareTrips` predicate; FeedsService and the plugin RPC host inject `TripsService`; a 3-export `trips.bridge.ts` serves the legacy prompts registrar and budget.mcp.ts's owner/member seam; todo/share/collab/vacay bridges deleted with their last consumers; the controller adopted `trips.dto.ts`, clearing all seven trip allow-list entries). placeService followed (done 2026-07 — the 1029-line place core folded into the wrapper `PlacesService` with its pure half in `places.helpers.ts`; the 10-tool registrar + the trip-places resource moved to the decorator-driven `places.mcp.ts` (`search_place` injects `MapsService`); TripsService/DaysMcp/BookingImportService/the plugin RPC host all inject it, so the domain needs **no bridge at all**; the sibling `placeEnrichment` fold absorbed the last `maps.bridge` consumer and deleted that bridge too, and the DTO ratchet cleared all seven `PlacesController` allow-list entries). transitItineraryService followed (done 2026-08 — the last step-4 link and the first pure-helpers relocation with no service fold: the 287-line module is 100% pure (no SQL, no DB, no broadcasts), so the schemas + endpoint/metadata builders moved byte-identical to `nest/transit/transit-itinerary.helpers.ts` (the schemas must stay module-level plain exports — `transit.mcp.ts` consumes them inside `@Tool` decorators, which evaluate before the container exists), its sole consumer `transit.mcp.ts` was a one-import repoint, and a new 21-case `TRANSIT-ITIN-*` characterization suite pins the previously untested superRefine error strings and builder output — **step 4 is complete; the notifications fan-in heads the frontier**). Wave 5 — the heavyweights, last 10. adminService (14 Nest + 11 MCP consumers), journeyService/atlasService/vacayService (done 2026-07 — pulled forward as a zero-dependency frontier member, see migration-graph.md)/collectionsService/mapsService (done 2026-07 — pulled forward as step 4 of the dependency-honest order: the 1429-line geo core folded into the wrapper `MapsService`, pure helpers to `maps.helpers.ts`, the 3 geo MCP tools to the decorator-driven `maps.mcp.ts` (weather/airport tools stay in the legacy registrar), BookingImportService injects it, and a 3-export `maps.bridge.ts` serves placeEnrichment + the places registrar — transitService → placeService now unblocked)/transitService (done 2026-07 — pulled forward as step 5 of the dependency-honest order: the first fully SQL-free domain fold — the 333-line Transitous/MOTIS proxy became a dep-free `TransitService` (no `DatabaseService`); pure `deriveTransitStats` + mode whitelist + itinerary types moved to `transit.helpers.ts` so the downstream `transitItineraryService` needed no bridge; the whole 3-tool registrar moved to the decorator-driven `transit.mcp.ts`, its days/reservations bridge imports becoming injected `DaysService`/`ReservationsService` — zero bridge files; placeService → transitItineraryService followed, both done 2026-07/08), and finally the auth stack (authService at 1494 lines with 18 MCP consumers, plus oauthService/oidcService/passkeyService) — highest risk, most consumers, security-sensitive; do it when the pattern is thoroughly proven. backupService also stays late: it owns closeDb/reinitialize lifecycle, which is deliberately outside the DI wrapper. Two special cases to keep in mind: airportService is lazily required by db/database.ts at boot (the flight-endpoint backfill) — whenever it migrates, that call should move into Nest's bootstrap instead, or you recreate the circular import; and notifications/notificationService are imported by 6+ other services, so like the wave-4 cluster they should migrate before their dependents or keep function shims in the interim. The through-line: each finished migration lets that domain's tests drop vi.mock('db/database') for overrideProvider(DATABASE_CONNECTION), so the mock seam shrinks wave by wave and disappears with the auth stack. Want me to start on tagService as the pilot?