# Configuration reference The notification delivery platform binds under `ca-skeleton.notification.platform`. The tree lives in `src/app-bootstrap/src/main/resources/application.yml`, disabled by default, and every value carries an inline default so a deployment that leaves the platform off supplies nothing. Until 2026-08-15 this page named properties the binding did not have — `max-retry-concurrency`, `scheduler-poll-interval`, `callback-worker-concurrency` — and omitted three it did. There was no tree in `application.yml` at all, so the only way to configure the platform was to guess environment variable names from Boot's relaxed binding. `./gradlew verifyNotificationConfiguration` now fails when this page, the YAML tree and `docs/registries/env-keys.yaml` disagree. ## Master switch | Property | Environment variable | Default | Meaning | |---|---|---|---| | `enabled` | `APP_NOTIFICATION_PLATFORM_ENABLED` | `false` | Binds nothing at all while false: no runtime, no schema check, no scheduler thread, no secret required | | `mode` | `APP_NOTIFICATION_PLATFORM_MODE` | `SERVING` | `SERVING` refuses to start without a working provider; `INGEST_ONLY` stores requests and does not dispatch | ## Dispatch | Property | Environment variable | Default | Bound | |---|---|---|---| | `dispatch.claim-batch-size` | `APP_NOTIFICATION_PLATFORM_CLAIM_BATCH_SIZE` | `50` | 1..1000 | | `dispatch.lease-duration` | `APP_NOTIFICATION_PLATFORM_LEASE_DURATION` | `2m` | positive, finite | | `dispatch.poll-interval` | `APP_NOTIFICATION_PLATFORM_POLL_INTERVAL` | `1s` | positive, finite | | `dispatch.max-global-concurrency` | `APP_NOTIFICATION_PLATFORM_MAX_CONCURRENCY` | `64` | positive | | `dispatch.max-additional-attempts` | `APP_NOTIFICATION_PLATFORM_MAX_ADDITIONAL_ATTEMPTS` | `4` | non-negative | | `dispatch.max-queue-age` | `APP_NOTIFICATION_PLATFORM_MAX_QUEUE_AGE` | `24h` | positive | | `dispatch.allow-ambiguous-fallback` | `APP_NOTIFICATION_PLATFORM_ALLOW_AMBIGUOUS_FALLBACK` | `false` | boolean | Every value is bounded. "Unlimited" is not an accepted configuration. The lease must outlast a provider call plus its timeout. Below that, a delivery a live worker is still waiting on gets claimed by a second worker, and the recipient receives the notification twice. ## Callbacks | Property | Environment variable | Default | Bound | |---|---|---|---| | `callbacks.enabled` | `APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED` | `false` | boolean | | `callbacks.max-body-bytes` | `APP_NOTIFICATION_PLATFORM_CALLBACK_MAX_BODY_BYTES` | `65508` | 1..65508 | | `callbacks.replay-skew` | `APP_NOTIFICATION_PLATFORM_CALLBACK_REPLAY_SKEW` | `5m` | positive | 65508 is not a round number by accident: it is the ciphertext column's 65536 bytes minus the AES-GCM nonce and tag. A larger configured value would pass every check above the database and fail the `CHECK` constraint after the callback had already been acknowledged to the provider. ## Provider profiles Profiles are a map under `providers`, keyed by profile id. There are no environment variables for them, because the keys are deployment-chosen; supply them as YAML or as `CA_SKELETON_NOTIFICATION_PLATFORM_PROVIDERS__`. | Field | Meaning | |---|---| | `type` | `APNS`, `FCM`, `SES`, `SMTP`, `TWILIO`, `WEB_PUSH`, `WEBHOOK` — a closed enum, so an unknown value fails binding rather than assembling into nothing | | `enabled` | A disabled profile is bound and validated but contributes no runtime | | `primary-for-channel` | Exactly one primary per channel | | `environment` | Required when enabled | | `credential-profile` | Resolved through `SecretMaterialProvider`; never an inline secret | | `topic` | APNs bundle id | | `vapid-public-key` | Web Push application server key | | `callback-signing-secret-ref` | Reference, not material | | `timeout` | Positive and finite | | `max-concurrency` | Positive | | `rate-per-second` | Positive | A profile pins provider type, environment, credential profile, timeouts, concurrency and rate limit. Sender identity and credential profile are separate concerns. ## Startup failures Startup fails rather than degrading when: - a payload or queue setting is unbounded - a timeout is negative - a TTL-required profile has no expiry source - a callback signing secret is missing - a production profile enables trust-all - an APNs profile is missing its environment or topic - a Web Push profile is missing its VAPID key - two provider profiles share an id - a route points only at disabled providers - `mode` is `SERVING` and no provider profile is enabled - the notification schema stream is not applied and promoted ## Secrets All key material arrives through `SecretMaterialProvider`. Nothing is read from source, from a committed file, or from a plaintext log. Contact point encryption and lookup HMAC keys must be distinct, and the encryption key must be exactly 256 bits. ## Readiness The platform contributes a `notifications` actuator endpoint and a health indicator. It reports DOWN when a provider's credentials were rejected, when a configured provider has no channel route, and when the measured backlog, stuck-lease count, projection lag or reconciliation lag passes the thresholds in `NotificationServingThresholds`. See [operations.md](operations.md).