# NotificationSubscription

## Description

NotificationSubscription provides follower-style relationships between users and arbitrary domain records — the same model as Odoo's chatter "followers", Knock's Subscriptions, and Novu's Topics. A subscriber explicitly registers interest in a `(sourceType, sourceId)` pair and the row records `userId`, `sourceType`, `sourceId`, and `subscribedAt`. Subscriptions are scoped to one row per `(userId, sourceType, sourceId)` triple and the subscribe/unsubscribe surface is idempotent: subscribing twice returns the existing row without creating a duplicate, unsubscribing when no row exists is a no-op success.

The `sourceType + sourceId` reference is polymorphic — the subscription does not validate that the source record exists, store any source content, or hold a foreign key back to the emitting module's schema. This mirrors the decoupling pattern used elsewhere in the notification module (Notification, NotificationDeliveryAudit). Audience union for an event is performed **inside the dispatcher**: at dispatch time it reads this entity by the event's `(sourceType, sourceId)` to resolve `SUBSCRIBED` watchers, unions them with the emitter's reason-tagged hints (assignees, authors, mentions), dedups by userId keeping the highest-precedence reason, and suppresses the actor unless `notifySelf` is set. Emitter-side recipient hints belong in `NotificationEvent.payload.recipients`; this entity only stores the user's explicit persistent registration.

Subscriptions are self-managed: a user can only subscribe or unsubscribe themselves. Cross-user subscribe/unsubscribe attempts are rejected at the command boundary.

## Domain Model Definitions

### Model type

Standard

### Command Definitions

- [createNotificationSubscription](../command/CreateNotificationSubscription.md) - Idempotently insert a `(userId, sourceType, sourceId)` row with `subscribedAt = now`
- [deleteNotificationSubscription](../command/DeleteNotificationSubscription.md) - Idempotently remove a `(userId, sourceType, sourceId)` row

### Query Definitions

- [listNotificationSubscriptions](../query/ListNotificationSubscriptions.md) - List the caller's own subscription rows for self-service inspection, optionally narrowed by `(sourceType, sourceId)`; dispatcher audience resolution reads NotificationSubscription directly and does not use this query

### Models

- NotificationSubscription

### Invariants

- The triple `(userId, sourceType, sourceId)` is unique; a concurrent retry of the same subscribe call collapses to a single row at the storage layer
- `subscribedAt` is set at first insert and is not refreshed by repeat subscribe calls on the same triple
- Resubscribing after an unsubscribe inserts a new row with a fresh `subscribedAt` timestamp; the previous row was deleted, not soft-deleted
- A user can only subscribe or unsubscribe themselves; any cross-user attempt is rejected with `FORBIDDEN`
- The source record is not validated to exist at subscribe time; a row referencing a non-existent or later-deleted source is harmless and is not auto-cleaned
- `sourceType` is part of the lookup key; a list query on one `sourceType` never returns subscriptions on a different `sourceType` even when `sourceId` collides
- Unsubscribing when no matching row exists is a successful no-op and persists zero changes

### Relationships

- **References User (user-management) as userId**: the subscriber whose interest the row records
- **References polymorphic source as `(sourceType, sourceId)`**: the followed record, declared by string discriminator with no foreign-key constraint to the emitting module's schema
- **Read by the dispatcher during audience resolution**: `dispatchNotification` reads rows by the event's `(sourceType, sourceId)` to resolve `SUBSCRIBED` watchers internally — emitters never pre-resolve or merge the watcher list themselves
