diplomail (Stage A): add in-game personal mail subsystem
Tests · Go / test (push) Successful in 1m44s
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · Go / test (pull_request) Successful in 2m45s

Phase 28 of ui/PLAN.md needs a persistent player-to-player mail
channel; the existing `mail` package is a transactional email
outbox and the `notification` catalog is one-way platform events.
Stage A lands the schema (diplomail_messages / _recipients /
_translations), a single-recipient personal send/read/delete
service path, a `diplomail.message.received` push kind plumbed
through the notification pipeline, and an unread-counts endpoint
that drives the lobby badge. Admin / system mail, lifecycle hooks,
paid-tier broadcast, multi-game broadcast, bulk purge and language
detection / translation cache come in stages B–D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-15 18:28:55 +02:00
parent 77cb7c78b6
commit 535e27008f
28 changed files with 3069 additions and 12 deletions
+42 -9
View File
@@ -192,10 +192,12 @@ because they cross domain boundaries:
`race_name`) remain `text`.
- Foreign keys are intra-domain only: `accounts → entitlement_*` /
`sanction_*` / `limit_*`; `games → applications` / `invites` /
`memberships` (with `ON DELETE CASCADE`); `mail_payloads →
mail_deliveries → mail_recipients` / `mail_attempts` /
`mail_dead_letters`; `notifications → notification_routes` /
`notification_dead_letters`. Cross-domain references
`memberships` / `diplomail_messages` (each with
`ON DELETE CASCADE`); `mail_payloads → mail_deliveries →
mail_recipients` / `mail_attempts` / `mail_dead_letters`;
`notifications → notification_routes` / `notification_dead_letters`;
`diplomail_messages → diplomail_recipients` /
`diplomail_translations`. Cross-domain references
(`memberships.user_id`, `games.owner_user_id`, etc.) are kept as
opaque `uuid` columns because each domain runs its own cleanup
through the in-process cascade described in [§7](#7-in-process-async-patterns). Adding a database
@@ -456,12 +458,15 @@ committed; SMTP completion is asynchronous to the auth request.
Notifications are an in-process pipeline. The closed catalog is
defined in `backend/internal/notification/catalog.go` and currently
covers 13 kinds: 10 lobby kinds (invite received/revoked, application
covers 16 kinds: 10 lobby kinds (invite received/revoked, application
submitted/approved/rejected, membership removed/blocked, race name
registered/pending/expired) and 3 admin-recipient runtime kinds
(image pull failed, container start failed, start config invalid).
Per-kind delivery channels (push, email, or both) and the admin-vs-
per-user recipient routing live in the same file.
registered/pending/expired), 3 admin-recipient runtime kinds (image
pull failed, container start failed, start config invalid), 2 game
lifecycle kinds (turn ready, game paused), and the
`diplomail.message.received` kind that fans diplomatic-mail send
events out to the recipient's push stream. Per-kind delivery channels
(push, email, or both) and the admin-vs-per-user recipient routing
live in the same file.
For every intent, `notification.Submit` performs:
@@ -490,6 +495,34 @@ Notification persistence is the auditable record of "we tried to tell
this user about this thing"; clients still derive their actual game
state through normal user-facing reads.
### 12.1 Diplomatic mail subsystem
`backend/internal/diplomail` owns the player-to-player message channel
that the in-game mail view consumes. The data lives in three tables:
- `diplomail_messages` — one canonical row per send. Captures the
game name and the sender IP at insert time so audit rendering
survives game renames and bulk purges. `kind` is `personal` (a
replyable player→player message) or `admin` (a non-replyable
notification produced by an administrator or the system).
`sender_kind` distinguishes `player`, `admin`, and `system` senders.
`broadcast_scope` carries `single`, `game_broadcast`, or
`multi_game_broadcast`.
- `diplomail_recipients` — one row per (message, recipient). Holds
the per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at`
state plus snapshot fields (`recipient_user_name`,
`recipient_race_name`) so admin search and the inbox listing render
correctly even after the source rows are renamed or revoked.
- `diplomail_translations` — cached per-language rendering shared
across every recipient with the same `accounts.preferred_language`.
Stage A wires the personal subset (single recipient, no language
detection). Lifecycle hooks (paused / cancelled / kicked), paid-tier
player broadcasts, multi-game admin broadcasts, bulk purge, and the
detection / translation cache land in later stages. The package is
the only place that constructs `diplomail.message.received` push
intents; the notification pipeline takes it from there.
## 13. Container Lifecycle (in-process)
`backend/internal/runtime` owns the lifecycle of game-engine containers