# 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: truewhen 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.