# Why V2?

Short answer: V1 worked for one app. V2 is built for every Inovus app — safely.

V1 lived inside Video Library (table + edge function + in-app notificationService). It solved a real product need, but it accumulated sharp edges: a shared API key, an overly open database policy, review-tool-specific dedupe, a hard 50-item limit, and “realtime” that never actually worked.

V2 is a redesign, not a copy-paste. Same mental model (per-user inbox + severities + action links), different foundation.


# One-paragraph version

V1 was a Supabase-embedded feature of one product, writable with a shared secret, with fragile dedupe and silent realtime failure. V2 is a standalone Cloudflare Worker + npm client: real OAuth for publishers, race-safe idempotency, pagination, an OpenAPI contract, honest polling, and a migration path that keeps old HTTP callers working while you move.


# What got better (the highlights)

flowchart LR
  subgraph v1 [V1]
    VL["Owned by Video Library"]
    Key["Shared API key"]
    Open["Permissive DB insert"]
    RT["Broken realtime"]
  end

  subgraph v2 [V2]
    Standalone["Shared service"]
    OAuth["Cognito scopes / groups"]
    Trusted["One trusted write path"]
    Poll["Honest ETag polling"]
  end

  VL --> Standalone
  Key --> OAuth
  Open --> Trusted
  RT --> Poll
Theme V1 pain V2 fix
Security Open insert policy + shared x-api-key Deny-all RLS; only the Worker writes; Cognito tokens for publishers
Identity Producer identity on trust source derived from the token — can’t spoof
Reuse One app owned the table Any app publishes / consumes via HTTP + npm client
Dedupe Review-case special case; racey check-then-insert Generic dedupeKey; DB unique index
Delivery Realtime claimed, polling actually used Polling is the design; 304s keep it cheap
Client Leaked channels on logout; no retries subscribe() returns unsubscribe; retries + timeouts built in
Contract Prose docs drifted OpenAPI generated from code, CI fails on drift

# What stayed the same (on purpose)

  • Per-user inbox keyed by Cognito sub
  • Severities: info | success | warning | error
  • Title, body, optional deep link (actionUrl)
  • ~30 second freshness as the default
  • Hard delete (no archive/snooze yet)
  • 200 + duplicate: true when an idempotent publish hits an existing key

# What V2 deliberately does not do

These aren’t unfinished tickets — they’re YAGNI until product demand is real:

  • No toast UI (apps keep their own)
  • No branded bell components
  • No email / SMS / mobile push
  • No SSE/WebSocket (polling first; same subscribe() API can grow later)
  • No notification preference centre

# Two kinds of “V1 compatibility”

People mix these up — they’re different:

Kind Meaning Status
V1 HTTP API Old /api/Notification/Create/... endpoints Fully supported on this service — see V1 compatibility
V1 app façade notification.success(title, message, url?) in the browser Supported via createNotificationService() — see Everyday usage

Pointing an old server at the new base URL is often enough for HTTP callers. Replacing the in-app singleton is a small adapter in your lib/notificationService.


# Migrating without a big-bang

flowchart TD
  A["1. Point V1 HTTP callers at new base URL"] --> B["2. Switch producers to client.publish + dedupeKey"]
  B --> C["3. Switch bell to NotificationsClient / useNotifications"]
  C --> D["4. Replace notification.success with createNotificationService"]
  D --> E["5. Decommission VL table + shared API keys"]

You can migrate producers and consumers independently — both APIs write the same rows.


# Want the full gap analysis?

This page is the friendly overview. The exhaustive V1→V2 mapping lives in docs/why-v2.md.

Next up for most people: Getting started.