Skip to content

feat: event-driven notification & delivery platform (#37) - #66

Merged
josueazc merged 16 commits into
Velar-Bonds:mainfrom
Villarley:feat/issue-37-notification-platform
Jul 28, 2026
Merged

feat: event-driven notification & delivery platform (#37)#66
josueazc merged 16 commits into
Velar-Bonds:mainfrom
Villarley:feat/issue-37-notification-platform

Conversation

@Villarley

Copy link
Copy Markdown
Contributor

Summary

Implements issue #37 — an event-driven notification & delivery platform extending VELAR's existing minimal in-app notification system. Everything is exercisable via in-memory fakes and property/concurrency tests: no VELAR database, secrets, or external API credentials required for npm run build / npm run lint / npm run test in either apps/api or apps/web.

Full design doc: docs/notifications/ARCHITECTURE.md. Reference docs updated: docs/BACKEND.md §10, docs/FRONTEND_GUIDE.md §15.

Architecture in one paragraph

This codebase has no application-level DB transactions (no .rpc() usage anywhere), so reliable outbox capture is done via Postgres AFTER INSERT/UPDATE triggers on bonds/transfers/reports that write to a monthly-partitioned outbox_events table in the same transaction as the domain write — zero changes to existing service call sequencing. A polling OutboxDispatcher drains the outbox with per-recipient ordering (hand-rolled async semaphore + promise chaining), exponential backoff + full jitter, per-channel circuit breakers, and a dedup store proving exactly-once-ish delivery under retries/replay. A pure, property-tested routing engine (DST-correct quiet hours via luxon, digest cadence/windowing, per-category/channel opt-outs) decides which channels fire. Channel adapters (in-app real, email/web-push behind Noop*Provider stubs pending real credentials) render through a versioned, i18n, XSS-sanitized template engine. A resumable realtime transport (cursor/catch-up/receipts) ships with an in-memory fake and a ws-based real implementation (not yet wired to a live gateway — documented as a known, intentional gap). Observability (metrics, tracing hooks, admin SLI endpoint) and security (rate limiting, HMAC payload signing, reused auth guards + RLS) round it out.

What's included

  • Migrations (additive only, never touching applied files): 20260702000000_notification_platform.sql (outbox + triggers + dedup/preferences/quiet-hours/digest-settings/receipts/DLQ/archive tables, RLS via the existing public.auth_role() helper), 20260703000000_notification_digest_queue.sql. Both functionally validated against a throwaway local Postgres (not a VELAR database) — triggers fire exactly once per real status transition, RLS owner-isolation and tse/admin-only DLQ visibility verified live, not just read.
  • Types: packages/types/src/notifications/* (domain events, preferences, routing, delivery) plus additive enum values in notification.ts/audit.ts.
  • Backend (apps/api/src/notifications/): outbox dispatcher core, dedup/preferences/recipient-directory/DLQ stores (in-memory + Postgres), routing engine, channel adapters, template engine, realtime transport, Postgres adapters + digest compiler, module wiring, inbox/preference-center endpoints (all registered in the shared apiContracts contract-validation system), observability + security.
  • Gap-fills (additive, matching each file's existing pattern, per docs/AGENTS.md §3 — nothing rewritten): BondsService.requestBond(), TransfersService.rejectReturn(), and ReportsService.create()/review() previously had zero audit/notification wiring; now they do.
  • Frontend: /notificaciones full inbox (filters, search, grouping, bulk actions, infinite scroll via keyset pagination), /configuracion/notificaciones preference center, a live-badge abstraction (PollingLiveSource in production, InMemoryLiveSource fake), and the existing bell refactored onto that same abstraction without behavior changes.

Verified

  • apps/api: 276 tests passing (concurrency/property tests for exactly-once delivery, per-recipient ordering, backpressure/DLQ; property tests for DST-correctness and digest windowing; XSS/snapshot tests for templates), tsc --noEmit clean, npm run build/lint clean.
  • apps/web: 42 tests passing, tsc/lint clean, npm run build clean (confirmed the one prerender quirk hit during verification — Next.js needing NEXT_PUBLIC_SUPABASE_* publishable config to prerender any Supabase-client page — is pre-existing on main, not introduced here; verified via a control build against origin/main).
  • One bug caught during manual browser verification and fixed: /notificaciones was missing from middleware.ts's protected-route list (separate commit).

Known, documented gaps (not silently papered over)

  • Realtime transport uses InMemoryRealtimeTransport in production wiring today; WebSocketRealtimeTransport exists but isn't wired to a live HTTP-upgrade/gateway endpoint yet.
  • Email/web-push channels use no-op providers by design (no external credentials in scope for this issue) — swapping in a real provider is a one-line change per the interface.
  • TransfersService.rejectReturn() doesn't change transfers.status, so it doesn't trigger the outbox (only the direct audit/notification calls added here cover it) — documented rather than worked around, to avoid changing existing status-transition behavior outside this issue's scope.

Test plan

  • npm run build / npm run lint / npm run test pass in apps/api and apps/web with no credentials set
  • Migration chain (including the two new files) applies cleanly against a throwaway local Postgres; triggers and RLS verified functionally
  • Manual browser check of /notificaciones and /configuracion/notificaciones render/redirect correctly (full login flow not exercised — would require live Supabase credentials, out of scope)
  • Reviewer: confirm campaign requirements (X follow, repo star, org follow) separately — those are account actions, not part of this diff

🤖 Generated with Claude Code

Villarley and others added 16 commits July 26, 2026 16:08
…s, receipts, DLQ, archival (Velar-Bonds#37)

Adds a monthly-partitioned outbox_events table populated via AFTER
INSERT/UPDATE triggers on bonds/transfers/reports, giving true
same-transaction outbox capture with zero changes to existing service
code (this repo has no app-level DB transactions). Also adds
notification_dedup, notification_preferences, notification_quiet_hours,
notification_digest_settings, notification_receipts, notification_dlq,
and additive columns + an archive table for the existing notifications
table. All RLS policies reuse the existing public.auth_role() helper
to avoid the recursion bug already fixed once in this repo.

Validated end-to-end against a throwaway local Postgres (full migration
chain applies cleanly; bond/transfer/report triggers fire exactly once
per real status transition and correctly skip no-op updates; RLS owner
isolation and tse/admin-only DLQ visibility both verified functionally).
…, circuit breaker, DLQ, dedup (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…reference routing engine (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…elar-Bonds#37)

The migration/architecture doc said "days" used ISO weekday numbering
(0=Monday), but packages/types and the routing engine implementation
both correctly use 0=Sunday..6=Saturday (JS Date.getDay()). Fixed the
two doc/comment mentions to match the actual, tested implementation.
…ine with XSS sanitization (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…reconnection, receipts (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…nts/DLQ adapters + digest compiler (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…tification gaps in requestBond/rejectReturn/reports (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…-center endpoints (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ed payloads, metrics/SLI endpoint (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…e center (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
…ds#37)

Caught during manual browser verification: the new inbox route was
missing from middleware.ts's PROTECTED/matcher lists, so it skipped
the server-side auth redirect every other protected route gets and
fell through to client-side-only redirect. Now consistent with
/marketplace, /mis-bonos, /negociaciones, etc.
… provider plug-in points (Velar-Bonds#37)

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

@Villarley is attempting to deploy a commit to the josueazc's projects Team on Vercel.

A member of the Team first needs to authorize it.

@josueazc
josueazc merged commit 8fa0dc3 into Velar-Bonds:main Jul 28, 2026
4 of 5 checks passed
josueazc added a commit that referenced this pull request Jul 28, 2026
…ion-platform"

This reverts commit 8fa0dc3, reversing
changes made to 9c15350.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants