In-app notifications: emit events + localizable payloads - #227
Merged
Conversation
Reshape the notification catalog to the human-facing events worth surfacing, using WebhookEvent's naming where the two channels overlap: - drop the unused file:* events (uploaded/downloaded/verified) - rename connection:synced/desynced -> connection:sync.completed/failed - add pipeline:run.analyzed/completed/failed - keep member:invited/joined and system:announcement/report Update the enum helpers (drop is_file_event, add is_pipeline_event and the pipeline category). Migration edited in place. This aligns the catalog so a shared emitter can map overlapping events; emitting these events is a follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Notifications had a read API but only member:invited was ever created. Add a NotificationEmitter service that writes per-account notification rows and wire it at the domain events worth surfacing. Structured, localizable model (no server-rendered text): - account_notifications: drop title/message, store notify_type (the client-side localization key) + typed params (renamed from metadata). - Response nests a payload: NotificationPayload, a notifyType-tagged enum with one named *Params struct per variant, so each type documents its params in OpenAPI and the client renders the copy. Emitter (in ServiceState/DI, holds PgClient): - notify_account: honors the member's notification_events_app prefs (empty = all; the column now defaults to every event). - notify_account_direct: for a recipient who is not yet a member (member:invited targets the invitee). Wired events (all best-effort, log-and-continue): member:invited, pipeline:run.analyzed/completed/failed, connection:sync.completed/failed. member:joined and system:* remain defined but unemitted. Migrations edited in place (pre-launch); DB reset + regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lumns member:joined: on invite accept, notify the workspace's owners and admins (excluding the joiner). The broadcast is two queries regardless of size: notification_recipients_by_roles resolves role- and preference-filtered recipients in one query, then create_account_notifications batch-inserts their rows. Drop legacy fields superseded by typed params: - remove related_id/related_type (columns, index, model, emitter, invites); the tagged payload's params already carry the typed ids. - retire the dead account_notifications constraints (title/message/related type length) and rename metadata_size -> params_size, with matching error mappings. Emitted events now: member:invited, member:joined, pipeline:run.analyzed/ completed/failed, connection:sync.completed/failed. system:* remain defined but unemitted (no trigger yet). Migrations edited in place; DB reset + regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The notification read API existed, but only
member:invitedwas ever created. This adds aNotificationEmitterthat actually writes notifications, reshapes the event catalog to match the webhook vocabulary, and moves the copy to a client-localizable structured payload.Event catalog (realigned with WebhookEvent)
NotificationEventis now the human-facing set, named to match webhooks where they overlap:member:invited,member:joined,connection:sync.completed,connection:sync.failed,pipeline:run.analyzed,pipeline:run.completed,pipeline:run.failed,system:announcement,system:report.(Dropped the unused
file:*events; renamedconnection:synced/desynced→sync.completed/failed; added the pipeline-run trio.)Localizable, structured payload
No server-rendered text. A notification stores
notify_type(the localization key) + typedparams; the response nests apayload: NotificationPayload— anotifyType-tagged enum with one named*Paramsstruct per variant, so each type documents its params in OpenAPI and the client renders the copy.title/messagecolumns; renamedmetadata→params.related_id/related_type(superseded by the typed params); retired the dead length constraints, renamedmetadata_size→params_size.Emitter
NotificationEmitter(inServiceState/DI, holdsPgClient):notify_account— targeted, honors the member'snotification_events_appprefs (empty = all; the column now defaults to every event).notify_account_direct— for a recipient who isn't yet a member (member:invited→ the invitee).notify_workspace_roles— broadcast to members of given roles, preference-filtered, in two queries total (notification_recipients_by_roles+ a batch insert) regardless of recipient count.Wired events
All best-effort (log-and-continue).
system:announcement/system:reportare defined but unemitted (no trigger exists yet).Notes
GET /notifications/, mark-read) landed in Notifications: explicit mark-read, drop side effect from GET #226; this builds the write side on top.Testing
Full gate green (check / fmt / clippy / doc / test).
🤖 Generated with Claude Code