Files

11 KiB

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
callbacks.trusted-proxies APP_NOTIFICATION_PLATFORM_CALLBACK_TRUSTED_PROXIES (empty) CSV of peer addresses

여러 provider가 요청 URL에 서명하므로, 그 URL을 잘못 재구성하면 정상 webhook이 전부 서명 실패가 된다. trusted-proxies가 비어 있으면 forwarded 헤더를 믿지 않고 컨테이너가 관측한 값을 쓴다. 무조건 믿으면 아무 호출자나 자기 서명이 검증될 URL을 고를 수 있어 서명 자체가 무의미해진다. 로드밸런서 뒤에 있는 배포는 그 peer를 명시한다.

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_<ID>_<FIELD>.

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.

SMTP relay

The one provider profile the template ships, off. A deployment that wants the common case sets APP_NOTIFICATION_PLATFORM_SMTP_ENABLED=true and the relay address; one that wants a different profile id or a second family declares it in its own YAML instead.

The profile and the relay are separate tables below because they answer different questions. The profile says which provider serves EMAIL and under what limits; the relay says what the transport is. Host, port and credentials are not here at all — they stay spring.mail.*, because Spring already owns them and a second spelling would be a second thing to keep in step.

Property Environment variable Default Bound
providers.smtp.enabled APP_NOTIFICATION_PLATFORM_SMTP_ENABLED false boolean
providers.smtp.primary-for-channel APP_NOTIFICATION_PLATFORM_SMTP_PRIMARY true boolean; exactly one primary per channel
providers.smtp.environment APP_NOTIFICATION_PLATFORM_SMTP_ENVIRONMENT local required when enabled
providers.smtp.credential-profile APP_NOTIFICATION_PLATFORM_SMTP_CREDENTIAL_PROFILE default resolved through SecretMaterialProvider, never inline material
providers.smtp.timeout APP_NOTIFICATION_PLATFORM_SMTP_TIMEOUT 10s positive, finite
providers.smtp.max-concurrency APP_NOTIFICATION_PLATFORM_SMTP_MAX_CONCURRENCY 4 positive
providers.smtp.rate-per-second APP_NOTIFICATION_PLATFORM_SMTP_RATE_PER_SECOND 10 positive
Property Environment variable Default Bound
smtp.tls-mode APP_NOTIFICATION_PLATFORM_SMTP_TLS_MODE STARTTLS_REQUIRED STARTTLS_REQUIRED or IMPLICIT_TLS
smtp.sender-identity APP_NOTIFICATION_PLATFORM_SMTP_SENDER_IDENTITY no-reply@example.invalid address
smtp.connect-timeout APP_NOTIFICATION_PLATFORM_SMTP_CONNECT_TIMEOUT 5s positive, finite
smtp.read-timeout APP_NOTIFICATION_PLATFORM_SMTP_READ_TIMEOUT 10s positive, finite
smtp.write-timeout APP_NOTIFICATION_PLATFORM_SMTP_WRITE_TIMEOUT 10s positive, finite
smtp.max-concurrency APP_NOTIFICATION_PLATFORM_SMTP_DISPATCH_CONCURRENCY 4 positive

The TLS mode enum has no plaintext member. An unencrypted relay is refused by construction rather than by a validator somebody has to remember to run.

The default sender is an RFC 2606 reserved domain that resolves nowhere, so a deployment that forgot to set one produces a traceable bounce instead of mail apparently sent from an address it does not own.

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.

Eight purposes, eight keys. Each is base64 of at least 32 bytes and each must differ from every other; the platform decodes them at startup and refuses to boot if one is blank, short or shared. A blank value used to be skipped, which meant the platform started without the key and found out on the first contact point — in production, on a recipient's notification.

Every default below is unset, deliberately. Supply the values out of band, per environment. Do not write one into this table, into application.yml, into an .env file that is tracked, or into any example: a value that appears in the repository is a value that has been disclosed.

Purpose Key material Active key id
Contact point encryption APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY APP_NOTIFICATION_PLATFORM_CONTACT_ENCRYPTION_KEY_ID
Contact point lookup HMAC APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY APP_NOTIFICATION_PLATFORM_CONTACT_LOOKUP_HMAC_KEY_ID
Callback signing APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY APP_NOTIFICATION_PLATFORM_CALLBACK_SIGNING_KEY_ID
Callback fingerprint HMAC APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY APP_NOTIFICATION_PLATFORM_CALLBACK_FINGERPRINT_HMAC_KEY_ID
Provider credential encryption APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY APP_NOTIFICATION_PLATFORM_PROVIDER_CREDENTIAL_KEY_ID
Provider request lookup HMAC APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY APP_NOTIFICATION_PLATFORM_PROVIDER_REQUEST_LOOKUP_HMAC_KEY_ID
Payload encryption APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY APP_NOTIFICATION_PLATFORM_PAYLOAD_ENCRYPTION_KEY_ID
Web Push VAPID signing APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY APP_NOTIFICATION_PLATFORM_VAPID_SIGNING_KEY_ID

A key id is not secret — an id identifies key material without revealing it — but it is required, and it has no default on purpose. A constant id makes a rotation indistinguishable from the key it replaced, so nothing could decrypt what was written before it. Change the id in the same deployment that changes the material, and keep the superseded key readable under its old id until the data it wrote has been re-encrypted. The rotation sequence is in at-rest-threat-model.md.

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.