Skip to content

Web Push notifications foundation (CIRCLE-38)#107

Draft
HamptonMakes wants to merge 1 commit intomainfrom
hampton/circle-38-web-push-foundation
Draft

Web Push notifications foundation (CIRCLE-38)#107
HamptonMakes wants to merge 1 commit intomainfrom
hampton/circle-38-web-push-foundation

Conversation

@HamptonMakes
Copy link
Copy Markdown
Collaborator

Foundation for in-browser push notifications. No notifications are delivered yet — that lands in follow-up work hooked into comment-reply / mention events.

Linear: CIRCLE-38

What's here

Per-device subscription storage

  • New coplan_web_push_subscriptions table (FK to coplan_users) and CoPlan::WebPushSubscription model.
  • Stores endpoint, SHA256 endpoint digest (uniquely indexed since FCM endpoints can exceed indexable length), p256dh + auth keys, user agent, last-seen / last-delivered timestamps, and a delivered-counter.
  • upsert_for is idempotent and rescues RecordNotUnique for concurrent inserts; record_delivery! uses an atomic increment! so concurrent deliveries can't lose updates.
  • device_label returns friendly strings like "Chrome on macOS" / "Safari on iOS".

Service worker

  • Served from /coplan_service_worker.js (engine-mounted route), no auth, Cache-Control: no-cache. Rendered inline so reverse proxies that intercept X-Sendfile won't try to reach into the gem on disk.
  • Default scope only — push events fire regardless of scope, so we don't broaden it.
  • Push handler shows the notification; click handler focuses an existing same-origin tab and navigates it (preserving the URL hash for anchor deep links) or opens a new window.

Subscription endpoints

  • POST /web_push/subscription — upserts the browser subscription for current_user.
  • DELETE /web_push/subscription — scoped to current_user so a leaked endpoint can't unsubscribe someone else.
  • Both 503 when VAPID isn't configured.

Browser-side JS

  • coplan/web_push.js ES module with isSupported / permission / isSubscribed / subscribe / unsubscribe. Pinned via importmap.
  • Awaits navigator.serviceWorker.ready after register() so PushManager has an active worker (fixes the "no active Service Worker" failure on first subscribe).
  • VAPID public key and service worker URL discovered via meta tags rendered only when CoPlan.configuration.web_push_configured?.

Settings UI

  • Notifications card on the Settings page: enable/disable on the current device, status text, and a list of all known devices for the user with friendly names + delivery stats.
  • New CSS for the settings rows / device list. Also adds [hidden] { display: none !important; } so hidden actually hides .btn elements.

Configuration

  • CoPlan::Configuration gains vapid_public_key / vapid_private_key / vapid_subject and web_push_configured?.
  • New bundle exec rake coplan:web_push:generate_keys.
  • web-push gem added to engine gemspec.
  • Host config/initializers/coplan.rb ships a checked-in dev VAPID keypair behind ENV overrides so the Settings UI works out-of-the-box locally. Production should override via env / encrypted credentials.

Verification

  • bundle exec rspec → 814 examples, 0 failures.
  • New specs: WebPushSubscription model, subscriptions controller (POST/DELETE), service worker route.
  • Manually exercised the Settings card: enable / disable / re-enable, device row appears with a friendly name.

Out of scope (next PRs)

  • Actual push delivery service (web-push gem + VAPID signing) wired into comment-reply / mention events.
  • Opt-in banner that nudges commenters to enable notifications.
  • Production VAPID key wiring in coplan-square.

Generated with Amp

Sets up the plumbing for browser-based push notifications without yet
delivering any. Subsequent work hooks comment-reply/mention events into
this pipeline.

Engine
- coplan_web_push_subscriptions table + model with per-device records
  (endpoint digest, keys, UA, last_seen_at, last_delivered_at, counter).
- Idempotent upsert_for tolerant of concurrent insert races; atomic
  increment for delivery counters.
- Service worker served from /coplan_service_worker.js (engine-scoped,
  no auth, no-cache, rendered inline so reverse proxies don't intercept).
- Push handler shows notifications and routes notification clicks to a
  matching same-origin tab (preserves URL hash for anchor deep links).
- POST/DELETE /web_push/subscription endpoints, scoped to current_user.
- Shared coplan/web_push ES module (subscribe/unsubscribe/isSupported).
- Settings card to enable/disable per-device + list of known devices
  with friendly labels (Chrome on macOS, Safari on iOS, etc.).
- VAPID public key + service worker URL exposed via meta tags only when
  configured. Configuration gains vapid_public_key/vapid_private_key/
  vapid_subject and web_push_configured?.
- web-push gem dependency + bundle exec rake coplan:web_push:generate_keys.

Host
- Dev VAPID key pair wired into config/initializers/coplan.rb via ENV
  fallbacks so local Settings UI is testable out-of-the-box.

Generated with Amp

Amp-Thread-ID: https://ampcode.com/threads/T-019df459-b110-726a-97e2-ff15e2903435
Co-authored-by: Amp <amp@ampcode.com>
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.

1 participant