5.6 KiB
Static Notification Sync Protocol
This document defines the static notification file contract and database sync semantics.
Protocol verification status:
- Sync plan source:
docs/plans/static-notification-sync-plan.md - Static sync implementation source:
backend/src/core/config/notification/static_sync.py - Static sync schema source:
backend/src/core/config/notification/static_schema.py
Compatibility strategy
- Additive evolution only.
- Existing
source_keyvalues are stable identifiers and must not be repurposed. - Changing notification content must not reset user read state.
- Removing a file has no effect by default; database pruning requires explicit CLI flag.
Static file location
Static notification files live under:
backend/src/core/config/static/notification/notifications/*.yaml
The sync command scans all *.yaml files in that directory unless a specific source_key is requested.
File schema
Each YAML file contains two top-level sections:
notificationtargets
Example:
notification:
source_key: welcome_bonus
version: 1
type: system
status: published
published_at: 2026-04-10T08:00:00Z
title: 新用户欢迎通知
body: 你已获得注册奖励,可前往积分中心查看。
payload:
action: open_route
route: /points
tab: balance
targets:
mode: all_users
notification
source_key: required, string, max 128, unique among static notificationsversion: required, integer,>= 1type: required, string, currentlysystemstatus: required, one ofdraft,published,revokeddeleted: optional, boolean, defaultfalse, soft-delete this notificationpublished_at: optional ISO 8601 timestamptitle: required, non-empty stringbody: required, non-empty stringpayload: required, must follow the notification payload protocol
targets
mode: required, one ofall_users,user_idsuser_ids: required only whenmode = user_ids
Rules:
mode = all_users:user_idsmust be absentmode = user_ids:user_idsmust be a non-empty UUID list
Payload contract
notification.payload reuses the inbox notification payload schema.
action = "none"
payload:
action: none
action = "open_route"
payload:
action: open_route
route: /points
entity_id: optional-id
tab: balance
Rules:
route: required, max 200entity_id: optional, max 64tab: optional, max 32url: must be absent
action = "open_url"
payload:
action: open_url
url: https://example.com/page
Rules:
url: required, max 500route,entity_id,tab: must be absent
Database mapping
Static notifications map to notifications rows using:
source = 'static'source_key = notification.source_key
Additional notification fields:
source_version = notification.versioncontent_hash = normalized content hash
Required uniqueness:
UNIQUE(source, source_key)wheresource_key IS NOT NULL
Sync semantics
Create
If (source='static', source_key=...) does not exist:
- Create
notifications - Create
user_notificationsfor target users
Update
If (source='static', source_key=...) already exists:
- Update
title,body,payload,status,published_at,source_version,content_hash - Keep existing
user_notifications - Do not reset
is_readorread_at
Revoke
If notification.status = revoked:
- Update
notifications.status = 'revoked' - Set
revoked_at - Keep existing
user_notifications
Soft delete
If notification.deleted = true:
- Set
notifications.deleted_at - Keep existing
user_notifications - Exclude the notification from list/unread queries
Draft
If notification.status = draft:
- Keep or create the main
notificationsrow - Do not expose it to users through list/unread queries
- Do not create new
user_notificationsduring sync
File deletion
Deleting a YAML file has no database effect by default.
Reason:
- Prevent accidental revocation/deletion from filesystem changes
If the CLI is executed with --prune, then static notifications missing from the scanned files are soft-deleted by setting deleted_at.
Target changes
Default behavior:
- Newly added targets receive new
user_notifications - Removed targets do not delete existing
user_notifications
This is intentionally non-destructive.
If the CLI is executed with --reconcile-targets, existing user_notifications that are no longer part of the computed target set are deleted.
CLI contract
Backend CLI command:
PYTHONPATH=backend/src uv run python -m core.runtime.cli sync-notifications
Supported options:
--path <dir-or-file>: override static notification source path--source-key <key>: sync only one notification--dry-run: validate and compute changes without writing to DB--prune: soft-delete static notifications missing from the scanned files--reconcile-targets: delete extrauser_notificationsnot in the current target set
Infra wrapper:
./infra/scripts/register-notifications.sh
./infra/scripts/register-notifications.sh --dry-run
./infra/scripts/register-notifications.sh --source-key welcome_bonus
./infra/scripts/register-notifications.sh --prune --reconcile-targets
Failure behavior
- Invalid YAML structure must fail the sync run
- Duplicate
source_keyacross files must fail the sync run - Invalid payload structure must fail the sync run
- Missing target users are not auto-created
- Database write failure must fail the sync run
No partial silent success is allowed.