asyncapi: 3.1.0 info: title: Notification Service Intent Contract version: 1.0.0 description: | Stable Redis Streams contract for normalized notification intents published by upstream services toward Notification Service. channels: intents: address: notification:intents messages: notificationIntent: $ref: '#/components/messages/NotificationIntent' operations: publishNotificationIntent: action: send summary: Publish one normalized notification intent. channel: $ref: '#/channels/intents' messages: - $ref: '#/channels/intents/messages/notificationIntent' components: messages: NotificationIntent: name: NotificationIntent title: Notification intent summary: One normalized notification request published into Notification Service. payload: $ref: '#/components/schemas/NotificationIntentEnvelope' examples: - name: gameTurnReady summary: User-targeted game-turn notification. payload: notification_type: game.turn.ready producer: game_master audience_kind: user recipient_user_ids_json: '["user-1","user-2"]' idempotency_key: game-master:game-123:turn-54 occurred_at_ms: "1775121700000" request_id: request-123 trace_id: trace-123 payload_json: '{"game_id":"game-123","game_name":"Nebula Clash","turn_number":54}' - name: geoReviewRecommended summary: Administrator email notification. payload: notification_type: geo.review_recommended producer: geoprofile audience_kind: admin_email idempotency_key: geoprofile:user-123:review-true:1775121700001 occurred_at_ms: "1775121700001" payload_json: '{"user_id":"user-123","user_email":"pilot@example.com","observed_country":"DE","usual_connection_country":"PL","review_reason":"country_mismatch"}' - name: lobbyApplicationSubmittedPublic summary: Public-game application notification sent to configured admins. payload: notification_type: lobby.application.submitted producer: game_lobby audience_kind: admin_email idempotency_key: game-lobby:game-456:application-submitted:user-42 occurred_at_ms: "1775121700002" payload_json: '{"game_id":"game-456","game_name":"Orion Front","applicant_user_id":"user-42","applicant_name":"Nova Pilot"}' schemas: NotificationIntentEnvelope: type: object additionalProperties: false description: | Stable producer-to-notification envelope for one normalized notification intent. Duplicate handling is scoped by `(producer, idempotency_key)`. A replay with the same normalized content is a successful duplicate. A replay with different normalized content is a conflict. `request_id` and `trace_id` are observability-only metadata and do not participate in idempotency fingerprinting. required: - notification_type - producer - audience_kind - idempotency_key - occurred_at_ms - payload_json properties: notification_type: type: string enum: - geo.review_recommended - game.turn.ready - game.finished - game.generation_failed - lobby.runtime_paused_after_start - lobby.application.submitted - lobby.membership.approved - lobby.membership.rejected - lobby.invite.created - lobby.invite.redeemed - lobby.invite.expired description: | Exact v1 notification type catalog. `lobby.invite.revoked` deliberately remains outside the supported catalog because it produces no notification. producer: type: string enum: - geoprofile - game_master - game_lobby description: | Stable producer identifier. The exact producer value is frozen per `notification_type` by the v1 catalog. audience_kind: type: string enum: - user - admin_email description: | Delivery audience selector. `user` targets concrete `user_id` values from the producer. `admin_email` targets configured administrator email lists. recipient_user_ids_json: type: string description: | JSON-encoded array of unique stable `user_id` values. Required for `audience_kind=user`. Forbidden for `audience_kind=admin_email`. `Notification Service` treats the recipient set as unordered for idempotency purposes: duplicate `user_id` values are invalid and element order does not change normalized content. contentMediaType: application/json contentSchema: type: array minItems: 1 uniqueItems: true items: type: string minLength: 1 idempotency_key: type: string minLength: 1 description: | Producer-owned idempotency key scoped together with `producer`. occurred_at_ms: type: string pattern: '^[0-9]+$' description: Milliseconds since Unix epoch as a base-10 string. request_id: type: string description: Optional observability request identifier. trace_id: type: string description: Optional observability trace identifier. payload_json: type: string description: | JSON-encoded type-specific payload. Payload normalization ignores insignificant whitespace and object key order, while array order remains significant. Required payload fields are frozen per `notification_type`. contentMediaType: application/json contentSchema: type: object additionalProperties: true allOf: - if: properties: audience_kind: const: user required: - audience_kind then: required: - recipient_user_ids_json - if: properties: audience_kind: const: admin_email required: - audience_kind then: not: required: - recipient_user_ids_json - if: properties: notification_type: const: geo.review_recommended required: - notification_type then: properties: producer: const: geoprofile audience_kind: const: admin_email payload_json: contentSchema: $ref: '#/components/schemas/GeoReviewRecommendedPayload' - if: properties: notification_type: const: game.turn.ready required: - notification_type then: properties: producer: const: game_master audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/GameTurnReadyPayload' - if: properties: notification_type: const: game.finished required: - notification_type then: properties: producer: const: game_master audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/GameFinishedPayload' - if: properties: notification_type: const: game.generation_failed required: - notification_type then: properties: producer: const: game_master audience_kind: const: admin_email payload_json: contentSchema: $ref: '#/components/schemas/GameGenerationFailedPayload' - if: properties: notification_type: const: lobby.runtime_paused_after_start required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: admin_email payload_json: contentSchema: $ref: '#/components/schemas/LobbyRuntimePausedAfterStartPayload' - if: properties: notification_type: const: lobby.application.submitted required: - notification_type then: properties: producer: const: game_lobby payload_json: contentSchema: $ref: '#/components/schemas/LobbyApplicationSubmittedPayload' oneOf: - properties: audience_kind: const: user required: - audience_kind - properties: audience_kind: const: admin_email required: - audience_kind - if: properties: notification_type: const: lobby.membership.approved required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/LobbyMembershipApprovedPayload' - if: properties: notification_type: const: lobby.membership.rejected required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/LobbyMembershipRejectedPayload' - if: properties: notification_type: const: lobby.invite.created required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/LobbyInviteCreatedPayload' - if: properties: notification_type: const: lobby.invite.redeemed required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/LobbyInviteRedeemedPayload' - if: properties: notification_type: const: lobby.invite.expired required: - notification_type then: properties: producer: const: game_lobby audience_kind: const: user payload_json: contentSchema: $ref: '#/components/schemas/LobbyInviteExpiredPayload' GeoReviewRecommendedPayload: type: object additionalProperties: true required: - user_id - user_email - observed_country - usual_connection_country - review_reason properties: user_id: type: string minLength: 1 user_email: type: string minLength: 1 observed_country: type: string minLength: 1 usual_connection_country: type: string minLength: 1 review_reason: type: string minLength: 1 GameTurnReadyPayload: type: object additionalProperties: true required: - game_id - game_name - turn_number properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 turn_number: type: integer minimum: 1 GameFinishedPayload: type: object additionalProperties: true required: - game_id - game_name - final_turn_number properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 final_turn_number: type: integer minimum: 1 GameGenerationFailedPayload: type: object additionalProperties: true required: - game_id - game_name - failure_reason properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 failure_reason: type: string minLength: 1 LobbyRuntimePausedAfterStartPayload: type: object additionalProperties: true required: - game_id - game_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 LobbyApplicationSubmittedPayload: type: object additionalProperties: true required: - game_id - game_name - applicant_user_id - applicant_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 applicant_user_id: type: string minLength: 1 applicant_name: type: string minLength: 1 LobbyMembershipApprovedPayload: type: object additionalProperties: true required: - game_id - game_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 LobbyMembershipRejectedPayload: type: object additionalProperties: true required: - game_id - game_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 LobbyInviteCreatedPayload: type: object additionalProperties: true required: - game_id - game_name - inviter_user_id - inviter_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 inviter_user_id: type: string minLength: 1 inviter_name: type: string minLength: 1 LobbyInviteRedeemedPayload: type: object additionalProperties: true required: - game_id - game_name - invitee_user_id - invitee_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 invitee_user_id: type: string minLength: 1 invitee_name: type: string minLength: 1 LobbyInviteExpiredPayload: type: object additionalProperties: true required: - game_id - game_name - invitee_user_id - invitee_name properties: game_id: type: string minLength: 1 game_name: type: string minLength: 1 invitee_user_id: type: string minLength: 1 invitee_name: type: string minLength: 1