# Legatus Architecture

## Purpose

Legatus is the **communication backbone** of the Quizzman ecosystem. Every service that needs to emit events, notify users, open realtime channels, track presence, or deliver webhooks/push does so through Legatus — never by coupling to peer services directly.

## Architectural styles

| Style | Application |
|-------|-------------|
| Clean Architecture | Domain at the center; frameworks at the edges |
| Hexagonal | Ports in `legatus-core`; adapters in infra crates |
| DDD (pragmatic) | Aggregates: Notification, EventEnvelope, WebhookEndpoint |
| SOLID | Trait-based DI; single-responsibility workers |
| Event-driven | All cross-service communication via the bus |
| Repository + Service | Storage repos; use cases as application services |

Abstractions only where substitution is real (queue, push, presence). No “enterprise” layers without a consumer.

## Crate dependency graph

```
gateway / worker
    ├── auth
    ├── storage ──────► core ◄────── queue
    ├── presence ─────► core         │
    ├── websocket ────► core         │
    ├── webhook                      │
    ├── push ─────────► core         │
    ├── metrics                      │
    ├── proto                        │
    └── common ◄─────────────────────┘
sdk ──► proto, common
```

`core` depends only on `common`. No SQL, Redis, NATS, or Axum inside `core`.

## Runtime processes

### Gateway (`legatus-gateway`)

- REST `/api/v1/*`
- WebSocket `/ws` (or `/api/v1/ws` depending on mount)
- JWT verification, rate limiting, CORS, request IDs
- Publishes to NATS; writes durable state via repositories
- Holds in-process `ConnectionManager` (sticky WS per node)

### Worker (`legatus-worker`)

- Consumes JetStream subjects
- Notification delivery (push / webhook / channel bookkeeping)
- Event → webhook matching
- Periodic cleanup of expired / soft-deleted rows
- Horizontally scalable; competing consumers on durables

## Data plane

| Store | Role | Durability |
|-------|------|------------|
| PostgreSQL | Notifications, webhooks, devices, templates, event archive, preferences | Durable |
| Redis | Presence TTLs, rate-limit counters, ephemeral session cache | Ephemeral |
| NATS JetStream | Event bus, delivery commands, DLQ | Durable (stream) |
| In-memory DashMap | Live WebSocket connections on a node | Process-local |

## Scaling model

```
                 LB / Ingress
                      │
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼
     gateway       gateway       gateway
        │             │             │
        └──────┬──────┴──────┬──────┘
               ▼             ▼
            Redis         NATS JS
               ▲             │
               │             ▼
            worker ←────── worker
               │
               ▼
           PostgreSQL
```

- **Gateway**: scale on CPU / WS count. Use sticky sessions or a shared pub/sub bridge for cross-node WS fan-out (phase 2: NATS→WS bridge).
- **Worker**: scale on queue lag. Separate durables per workload (deliver / webhook / cleanup).
- **PostgreSQL**: partition-ready via UUID v7 time ordering; read replicas for feeds.
- **Redis**: cluster for presence at hundreds of thousands of sessions.

### WebSocket fan-out (current → target)

**Current (v0.1):** each gateway node owns its connections. `RealtimePublisher` publishes locally. Cross-node delivery relies on workers publishing commands that each node can subscribe to, or clients reconnecting.

**Target:** every gateway subscribes to `realtime.user.{id}` / `realtime.channel.{name}` on NATS and forwards to local sockets. Connection manager stays local; bus provides the mesh.

## Domain aggregates

### Notification

Created with N receivers × channels. Status machine:

`pending → processing → delivered | partial | failed | cancelled | expired`

Optimistic locking via `version`. Soft delete via `deleted_at`.

### EventEnvelope

Canonical bus message. Producers never publish raw JSON without the envelope. Subject: `events.{event_type}`.

### WebhookEndpoint

Filter by event pattern (`*`, exact, prefix*). HMAC-signed delivery.

## Ports (selected)

| Port | Adapter |
|------|---------|
| `NotificationRepository` | `PgNotificationRepository` |
| `EventBus` / `QueueProvider` | `NatsJetStream` (+ `MockQueue`) |
| `PresenceStore` | `RedisPresenceStore` |
| `RealtimePublisher` | `RealtimePublisherAdapter` |
| `PushProvider` | FCM / APNs / Web / Huawei stubs |

## Key decisions (ADR-style)

1. **UUID v7 PKs** — time-sortable feeds without `created_at` index hotspots.
2. **NATS JetStream first** — operationally light; `QueueProvider` keeps Kafka optional.
3. **Redis never durable** — crash-safe product state always in Postgres.
4. **No login in Legatus** — IdP is `id.quizzman.com`; Legatus verifies only.
5. **Service JWT for S2S** — Wiki/LMS/Exam/etc. use scoped tokens, not shared API keys as long-term design.
6. **Edition 2024 / Rust stable** — async traits natively; Axum 0.8.
7. **UI on `ui.quizzman.com`** — Legatus gateway serves API only. Notification UI is `@qmf/legatus` (QMF plugin), not a separate JS release from the API host.

## UI & CDN boundary

```text
legatus.quizzman.com              ui.quizzman.com/qmf/dist/
├── REST /api/v1                  ├── qmf.min.js          (QMF core + plugins)
├── WebSocket /api/v1/ws          └── legatus/
└── (no static UI)                    ├── legatus.min.js  (standalone)
                                      └── legatus.css
```

| Layer | Host | Responsibility |
|-------|------|----------------|
| Backend | `legatus.quizzman.com` | Events, notifications, WS, webhooks, push |
| UI framework | `ui.quizzman.com` | QMF core, themes, shared components |
| Legatus UI | `@qmf/legatus` on CDN | Bell, inbox, toast, realtime client |

Embed pattern for Quizzman apps:

```js
QMF.use(createLegatusPlugin({ endpoint: 'https://legatus.quizzman.com', mountTo: '#header-actions' }));
QMF.init({ appId: 'wiki' });
```

Legacy `/sdk/*` on the API host **301-redirects** to `ui.quizzman.com/qmf/dist/legatus/*`.  
Source: `/opt/QMF/packages/legatus` — see [SDK_UI.md](SDK_UI.md).


- JetStream ack + `max_deliver` → DLQ stream `LEGATUS_DLQ`
- Webhook exponential backoff → dead letter outcome
- Gateway graceful shutdown with drain timeout
- Health: `/live` (process), `/ready` (deps), `/health` (aggregate)
- Idempotent publishes via `Nats-Msg-Id` (event id)

## Security boundary

See [SECURITY.md](SECURITY.md). Gateway is the only public edge; workers are internal. mTLS-ready between services (deploy concern).

## Extensibility

| Extension | How |
|-----------|-----|
| Kafka bus | New `KafkaQueue: QueueProvider` |
| Email channel | Worker + qm-mail client implementing delivery |
| Realtime CRDT sync | New channel protocol on WS + topic ACL |
| Multi-tenant | `tenant_id` already on `EventEnvelope`; row-level later |
