# Notification Delivery Platform — module mapping > Source design: `notification-superpowers-package/docs/superpowers/specs/2026-08-10-notification-platform-design.md` > > Source plan: `notification-superpowers-package/docs/superpowers/plans/2026-08-10-notification-platform-implementation-plan.md` ## Why a mapping exists The plan was written against a hypothetical repository (`modules/notification/**`, root package `io.backend.skeleton.notification`, 31 Gradle projects). This repository is a fail-closed 19-leaf Clean Architecture template: `src/settings.gradle` rejects any registry that does not contain exactly the 19 modules in `src/config/architecture/modules.json`, and `verifyCleanArchitectureDependencies` rejects any project edge outside `allowed_dependencies`. Creating 31 new Gradle projects would violate HARD-STOP #5 of `AGENTS.md`. The package README anticipates this and instructs the implementer to map dependency catalog and package/file paths onto the host repository's rules while preserving the public contracts and reliability semantics. Every logical module of the plan is therefore implemented as a **package** inside the registered leaf that owns its responsibility. No public contract, evidence rule, or reliability semantic is dropped. ## Logical module → registered leaf | Plan module | Registered leaf | Package | |---|---|---| | `notification-core-api` | `application-core` | `dev.caskeleton.application.notification.platform.api` | | `notification-content-api` | `application-core` | `…platform.api.content` | | `notification-contact-api` | `application-core` | `…platform.contact` | | `notification-template-api` | `application-core` | `…platform.template` | | `notification-policy` | `application-core` | `…platform.policy` | | `notification-provider-spi` | `application-core` | `…platform.provider` | | `notification-callback-api` | `application-core` | `…platform.callback` | | `notification-email-api` | `application-core` | `…platform.email` | | `notification-sms-api` | `application-core` | `…platform.sms` | | `notification-push-api` | `application-core` | `…platform.push` | | `notification-webpush` (API half) | `application-core` | `…platform.webpush` | | `notification-inbox-api` | `application-core` | `…platform.inbox` | | `notification-admin-api` | `application-core` | `…platform.admin` | | `notification-security` (ports + redaction) | `application-core` | `…platform.security` | | `notification-observability` (ports) | `application-core` | `…platform.observation` | | `notification-dispatch-runtime` | `adapter:outbound:notification` | `dev.caskeleton.adapter.outbound.notification.platform.dispatch` | | `notification-security` (AES-GCM/HMAC impl) | `adapter:outbound:notification` | `…platform.security` | | `notification-template-thymeleaf` (reference renderer) | `adapter:outbound:notification` | `…platform.template` | | `notification-email-smtp` | `adapter:outbound:notification` | `…platform.provider.smtp` | | `notification-email-ses` | `adapter:outbound:notification` | `…platform.provider.ses` | | `notification-sms-twilio` | `adapter:outbound:notification` | `…platform.provider.twilio` | | `notification-push-fcm` | `adapter:outbound:notification` | `…platform.provider.fcm` | | `notification-push-apns` | `adapter:outbound:notification` | `…platform.provider.apns` | | `notification-webpush` (transport + crypto) | `adapter:outbound:notification` | `…platform.provider.webpush` | | `notification-webhook-extension` | `adapter:outbound:notification` | `…platform.provider.webhook` | | `notification-observability` (Micrometer impl) | `adapter:outbound:notification` | `…platform.observation` | | `notification-admin-runtime` | `adapter:outbound:notification` | `…platform.admin` | | `notification-reactor` | `adapter:outbound:notification` | `…platform.reactor` | | `notification-spring-boot-starter` | `adapter:outbound:notification` (+ `app-bootstrap` wiring) | `…platform.autoconfigure` | | `notification-persistence-jpa` | `adapter:outbound:persistence-jpa` | `dev.caskeleton.adapter.outbound.persistence.notification.platform` | | `notification-inbox-jpa` | `adapter:outbound:persistence-jpa` | `…persistence.notification.platform.inbox` | | `notification-callback-mvc` | `adapter:inbound:web` | `dev.caskeleton.adapter.inbound.web.notification.platform.callback` | | `notification-callback-webflux` | `adapter:inbound:web` | `…callback.reactive` | | `notification-testkit` | test source sets of the owning leaves | `…platform.testkit` | ## Dependency-direction consequences The plan's module DAG (`*-api` → `provider-spi`/`policy` → runtime/adapters → starter) is preserved by the leaf DAG that the registry already enforces: ```text application-core (all *-api, provider SPI, policy, callback contracts) ↑ ↑ ↑ adapter:outbound:notification adapter:outbound:persistence-jpa adapter:inbound:web ↑ ↑ ↑ app-bootstrap ``` Two plan edges cannot be expressed as project edges in this repository, and are replaced by ports: 1. `notification-email-ses`, `notification-sms-twilio`, `notification-push-fcm`, `notification-push-apns`, `notification-webpush`, `notification-webhook-extension` → `httpclient platform`. `adapter-outbound-notification` is not allowed to depend on `adapter-outbound-httpclient`. The provider adapters therefore call `dev.caskeleton.adapter.outbound.notification.platform.provider.http.NotificationHttpGateway`, an adapter-local port with a JDK `java.net.http.HttpClient` default implementation. `app-bootstrap` sees both leaves and is the supported place to substitute an implementation backed by the HTTP Client Platform (TLS/timeout/circuit-breaker/SSRF/dynamic-target policy reuse). 2. `notification-inbox-jpa` → `optional messaging outbox integration`. `adapter-outbound-persistence-jpa` may not depend on `adapter-outbound-messaging`; the inbox publishes through the existing persistence outbox tables plus the `NotificationInboxSignalPort` application port, and `app-bootstrap` binds the relay. ## Commit policy `AGENTS.md` pins commit policy to `human-only`. Step 5 (`git add` / `git commit`) of every plan task is therefore intentionally **not** executed by the agent; the working tree carries the change and the human owner commits. ## R1 public type disposition (NOTIF-ADR-005) NOTIF-ADR-005 makes `..notification.platform..` canonical and keeps the R1 namespace in place without new consumers. The ADR owns the rule; this table owns the list, so the two do not drift apart by being written twice. `NOTIFICATION_R1_AND_PLATFORM_DO_NOT_DEPEND_ON_EACH_OTHER` in `CleanArchitectureTest` enforces the boundary: production dependencies between the two namespaces are zero, and the only permitted exception is `dev.caskeleton.application.notification.compatibility.r1`. | Disposition | Meaning | Types | | --- | --- | --- | | `replace` (27) | the platform has an equivalent; new consumers use it | `ApplyNotificationReceiptCommand`, `ApplyNotificationReceiptResult`, `ApplyNotificationReceiptUseCase`, `Channel`, `InlineNotificationAttemptPort`, `NormalizedNotificationReceiptCommand`, `NotificationAttemptId`, `NotificationDeliveryId`, `NotificationDeliveryStorePort`, `NotificationDispatchCommand`, `NotificationDispatchResult`, `NotificationDispatchUseCase`, `NotificationEvidenceTrustSnapshot`, `NotificationFrozenPlan`, `NotificationPlanPort`, `NotificationPlanningResult`, `NotificationProviderAttemptPort`, `NotificationReceiptEventId`, `NotificationReceiptFact`, `NotificationReceiptIngressCapabilityDescriptor`, `NotificationReceiptProjection`, `NotificationReceiptStorePort`, `NotificationSignedEvidenceHeader`, `NotificationWriterInventoryEvidence`, `NotificationWriterInventoryEvidenceVerifierPort`, `ProviderAttemptOutcome`, `TargetAttemptOutcome` | | `bridge` (28) | conversion needed if an R1 caller remains; conversion lives only in the ACL | `InitializeNotificationWriterFencesCommand`, `InitializeNotificationWriterFencesOperation`, `InitializeNotificationWriterFencesResult`, `InitializeNotificationWriterFencesUseCase`, `NotificationAdmissionGateCommand`, `NotificationAdmissionGateUseCase`, `NotificationAppendResult`, `NotificationLegacyWriterPermitCommand`, `NotificationLegacyWriterPermitResult`, `NotificationLegacyWriterPermitUseCase`, `NotificationMaintenanceCommand`, `NotificationMaintenanceResult`, `NotificationMaintenanceUseCase`, `NotificationOperationsSnapshot`, `NotificationOperationsSnapshotQuery`, `NotificationOperationsSnapshotUseCase`, `NotificationRequestResult`, `ReconcileNotificationDeliveriesCommand`, `ReconcileNotificationDeliveriesResult`, `ReconcileNotificationDeliveriesUseCase`, `SwitchNotificationWriterOwnershipCommand`, `SwitchNotificationWriterOwnershipOperation`, `SwitchNotificationWriterOwnershipResult`, `SwitchNotificationWriterOwnershipUseCase`, `TerminalizeExpiredNotificationWriterPermitsCommand`, `TerminalizeExpiredNotificationWriterPermitsOperation`, `TerminalizeExpiredNotificationWriterPermitsResult`, `TerminalizeExpiredNotificationWriterPermitsUseCase` | | `retain` (45) | a concern the platform does not cover; left as it is | `ConsentCheckMode`, `EmailRecipientReference`, `Notification`, `NotificationAdmissionClass`, `NotificationAdmissionReadinessPort`, `NotificationApplicationException`, `NotificationCanonicalWriterFenceGuard`, `NotificationCanonicalWriterFencePort`, `NotificationCanonicalWriterRouteSet`, `NotificationCapabilityCompatibilityValidator`, `NotificationChannel`, `NotificationFaultScope`, `NotificationIntentAppendPort`, `NotificationIntentDraft`, `NotificationIntentId`, `NotificationKindId`, `NotificationKindPolicy`, `NotificationMaintenanceStorePort`, `NotificationMode`, `NotificationOperationsSnapshotPort`, `NotificationPort`, `NotificationProviderCapabilityDescriptor`, `NotificationReasonCode`, `NotificationRecipientReference`, `NotificationReconciliationPort`, `NotificationRouteId`, `NotificationRouteStrategy`, `NotificationStoreCapabilityDescriptor`, `NotificationTechnicalSuppressionPort`, `NotificationTemplateParameters`, `NotificationTemplateRef`, `NotificationTemplateValue`, `NotificationWriterCutoverPort`, `NotificationWriterOwnership`, `NotificationWriterQuiescenceAttestationPort`, `NotificationWriterRouteSet`, `RecordNotificationWriterQuiescenceAttestationCommand`, `RecordNotificationWriterQuiescenceAttestationOperation`, `RecordNotificationWriterQuiescenceAttestationResult`, `RecordNotificationWriterQuiescenceAttestationUseCase`, `RetryDisposition`, `SignedNotificationWriterInventoryManifest`, `SignedNotificationWriterQuiescenceManifest`, `SlackAudienceReference`, `SubmissionCertainty` | Total: 100 public types, every one classified. No type carries `@Deprecated(forRemoval = true)`: no removal release is fixed, and `forRemoval` without a date is a promise the codebase cannot keep. The boundary is enforced by the ArchUnit rule instead.