# Oudelaa microservices migration

## Current architecture

The backend is a NestJS modular monolith backed by MongoDB. Redis is used for cache,
BullMQ jobs, and the optional Socket.IO adapter. Domain modules call the in-process
`NotificationsService`. Public HTTP routes use the global `/api/v1` prefix and the
notifications Socket.IO namespace is owned by the same application.

## Phase 1 boundary

- `apps/api-gateway` is the new monorepo entry point. It deliberately bootstraps the
  existing `src/AppModule`, so the application remains runnable while modules are moved
  incrementally.
- `apps/notification-service` owns notification persistence, queries, unread counts,
  read mutations, and the `follow.request.approved` consumer.
- The four existing Flutter notification endpoints remain on the gateway. The gateway
  delegates them over Nest RMQ only when `NOTIFICATION_SERVICE_ENABLED=true`; otherwise
  it uses the original in-process service.
- Socket.IO authentication, namespace, rooms, and public event names remain in the
  gateway. Internal RMQ responses include realtime data which the gateway emits.
- Other notification producers stay in-process in phase 1. They can migrate one event
  at a time in later phases.

## Reliability and topology

The main durable queue is `oudelaa.notifications`. Rejected messages are dead-lettered
to the durable `oudelaa.notifications.dlq` queue through the default exchange. Consumers
use manual acknowledgement and a prefetch limit. Event handling retries three times
inside one delivery, then rejects without requeue. Gateway calls have a bounded timeout,
one configurable retry, and a small circuit breaker. Rabbit message headers carry
`correlationId` and `requestId`; logs are JSON records containing both.

Follow-request approval is the first true transactional-outbox flow. The request state,
follow relationship, user counters, and deterministic outbox event are committed in one
MongoDB transaction. No BullMQ or RabbitMQ call occurs inside that transaction. When both
phase-1 flags are enabled, a polling dispatcher atomically leases pending/failed events,
waits for the Notification Service RPC response, and only then marks the event processed.
Timeouts use bounded exponential backoff and eventually move the event to `dead`; expired
`processing` leases are recovered after restart. `OutboxService.listByStatuses()` and
`retryEvent()` are the intentionally non-public administrative surface in this phase.

`eventId` is stored on notifications under a unique partial index. The consumer checks
it before insert and also handles Mongo duplicate-key races. The existing unique index on
recipient/type/reference remains as an additional guard for follow approval records.

## Planned migration

1. Phase 1: monorepo shell, contracts/events/common libraries, gateway compatibility
   facade, and Notification Service.
2. Phase 2: move Auth and Users behind versioned internal contracts; centralize JWT
   verification primitives in `libs/auth`; add tracing/metrics in `libs/observability`.
3. Phase 3: extract Posts and Media; replace direct notification calls with outbox events.
4. Phase 4: extract Feed and Search read models, consuming post/user events.
5. Phase 5: extract Moderation, then stabilize cross-service workflows and data ownership.
6. Phase 6: remove transitional imports and local fallbacks after traffic comparison,
   replay tests, and a controlled cutover.

At every phase the gateway retains public paths and response envelopes. Internal event
contracts evolve additively and are versioned before any breaking change.

## Configuration and operating modes

`NOTIFICATION_SERVICE_ENABLED=false` and `OUTBOX_DISPATCHER_ENABLED=false` remain the
safe defaults. In that mode the gateway does not instantiate or connect an RMQ client;
the in-process notification path and public Flutter contracts remain unchanged. The
standalone-Mongo fallback exists only for this disabled legacy transition mode.

The reliable phase-1 path requires both flags to be `true`. Dispatcher interval, batch,
lease, attempt limit, and bounded backoff are configured by `OUTBOX_*`. RabbitMQ accepts
an injected `RABBITMQ_URL` or separate host, port, username, password, and vhost values.
TLS supports `amqps://`, CA/client material injected as base64 or mounted paths, peer
verification, and SNI. Passwords and URL userinfo are never logged.

`docker-compose.microservices.yml` is local-only. It deliberately uses guest/guest inside
the private Compose network, exposes management only on loopback, and runs a single-node
Mongo replica set so transactions are exercised locally. It is not a production template.
Queue names remain `oudelaa.notifications` and `oudelaa.notifications.dlq` to avoid an
undeclared migration; both are durable, non-exclusive, and non-auto-delete. RPC messages
are persistent, and consumers use manual acknowledgement with a prefetch limit.

## Production prerequisites

- Managed RabbitMQ (or an operated HA cluster), a dedicated vhost, non-guest
  least-privilege credentials, and TLS/amqps. Inject credentials and certificates from
  the deployment secret store; never copy local Compose values.
- A transaction-capable MongoDB replica set or sharded cluster. Gateway startup rejects
  dispatcher activation against standalone MongoDB.
- Production Redis, service-specific secrets, persistent storage where applicable, and
  network policy that permits RMQ/Mongo/Redis access only from the services.
- Liveness probes at `/api/v1/health` and `/health/live`; readiness probes at
  `/api/v1/health/ready` and `/health/ready`. A temporary RMQ outage fails readiness but
  does not fail liveness. RabbitMQ management must not be publicly exposed.
- Alerts for pending/failed/dead outbox counts, oldest event age, dispatcher failures,
  notification DLQ depth, RMQ connectivity, and duplicate-key activity.

### Controlled activation and rollback

1. Provision production RabbitMQ with TLS, a dedicated vhost, and least-privilege user.
2. Confirm MongoDB is a replica set/mongos and Redis is production-ready.
3. Deploy Notification Service with injected secrets and verify liveness/readiness.
4. Deploy Gateway with both phase-1 flags still `false`; verify the monolith path.
5. Inject the same RMQ endpoint/trust settings into Gateway without logging them.
6. Enable both flags on one canary Gateway replica.
7. Approve a controlled private follow and observe outbox
   `pending -> processing -> processed`, one notification, and one Socket emission.
8. Monitor retries, dead events, DLQ, readiness, latency, and duplicate-key metrics.
9. Expand the flags gradually; atomic leases make multiple Gateway replicas safe.
10. Roll back by setting both flags to `false`. Do not delete pending events or queues;
    diagnose or retry them before the next canary, then drain Notification Service.

Notification Service still uses the same Mongo database and the same `notifications`
collection as Gateway. This is a transitional shared-database model, not independent data
ownership; physical separation belongs to a later migration phase.
