Notification Database Tenant Isolation
Summary
The extracted service declares no global query filter. The monolith declares one on all three of its notification tables, with a super-admin bypass. Tenant isolation for Notification is therefore a property of which store is being read.
Audience
Security reviewers, engineers, architects, QA and support.
Tenant isolation
Extracted service — no global filter
OnModelCreating contains no HasQueryFilter call for any of the seven entities. The class documentation states the reasoning directly: the notification service is a cross-tenant consumer and orchestrator that observes every tenant's events, so tenant scoping is applied per query instead.
Consequence: isolation is opt-in per query. A query that omits a tenant predicate silently returns rows across every tenant, with no compile-time or runtime signal. Adding a new query without the predicate produces a cross-tenant read by default rather than by mistake being caught.
Tenant identifier
Every tenant-bearing entity stores TenantId as a nullable string of maximum length 80. Six of the seven entities carry it; delivery attempts do not, so an attempt row can only be attributed to a tenant by joining through its message identifier — and no such relationship is declared.
Null is meaningful rather than absent: on templates it denotes a global default that applies to every tenant without an override.
Where scoping is applied
| Query site | Tenant predicate |
|---|---|
| Template resolution during dispatch | Yes — tenant match, then a separate null-tenant fallback |
| Preference evaluation | Yes — tenant match |
| Recipient upsert | Yes — part of the unique-key lookup |
| Inbox recipient lookup | Yes — tenant compared directly |
| Template list endpoint | Only when a tenant value is supplied |
| Single message, attempts, retry, read-state | No |
| Failed queue, audit query, statistics | No |
| Idempotency check and lookup | No — keyed on event identifier alone |
Cross-tenant protection
There is none at the persistence layer of the extracted service. Protection depends entirely on each query author remembering the predicate, and the administrative read paths deliberately omit it.
Administrative bypass
The extracted service has no bypass mechanism because it has no filter to bypass.
The monolith does: each of its three notification query filters is an expression combining a super-admin flag with a current-tenant comparison, so a super-admin principal reads across tenants while an ordinary principal is confined to the current tenant. The template filter additionally admits rows whose tenant is null, exposing global templates to every tenant.
Filter gaps
- No global filter on any extracted entity.
- No tenant column at all on delivery attempts.
- No tenant predicate on the three cross-tenant administrative reads.
- No tenant predicate on the idempotency ledger, so event identifiers share one namespace across tenants.
- No database-level row security, schema-per-tenant or partitioning.
Classification
Partial — Implemented in the monolith, Not implemented as a global mechanism in the extracted service.
Requires confirmation
Whether the cross-tenant consumer posture is intended to persist, and whether administrative reads should become tenant-scoped, require confirmation.
Related Articles
See Also
Keywords
- Notification database
- Tenant Isolation
- Draft database documentation
Source References
microservices/src/notification-service/Infrastructure/NotificationDbContext.csData/AppDbContext.csmicroservices/src/notification-service/Api/NotificationEndpoints.csmicroservices/src/notification-service/Application/NotificationDispatcher.csmicroservices/src/notification-service/Application/TemplateEngine.csmicroservices/src/notification-service/Application/PreferenceEvaluator.cs
Revision Information
- Status: Draft
- Last reviewed: 2026-07-21
- Review cycle: Quarterly