Skip to content

Add Web Push (VAPID) notification framework#39

Open
radiantnode wants to merge 1 commit into
mainfrom
claude/push-notifications-framework-wejfcf
Open

Add Web Push (VAPID) notification framework#39
radiantnode wants to merge 1 commit into
mainfrom
claude/push-notifications-framework-wejfcf

Conversation

@radiantnode

Copy link
Copy Markdown
Owner

Summary

Stands up the framework for browser push notifications, end to end. A signed-in account can subscribe, and a notification can be sent to any account — by users.id UUID or username — from a script. No game-event triggers yet; those are an additive next step.

Transport is standards-based Web Push (VAPID) — self-hosted, no third party. Everything is off by default (PUSH_ENABLED unset → the key endpoint 503s and the client opt-in quietly no-ops).

The headline deliverable:

python scripts/send_push.py --user alice --title "Your turn" --body "Tap to roll" --url /ABCDE
# --user accepts a username or a users.id UUID

What's included

Backend

  • server/push.py — the single send path (send_to_user) + subscription storage. pywebpush encryption runs in a thread (never on a gameplay hot path), dead subs (404/410) are pruned, failures are metered and never raised.
  • server/push_routes.pyGET /push/vapid-public-key, JWT-authed POST /push/subscribe + /push/unsubscribe. Shared require_user JWT dependency added to server/auth.py.
  • migrations/008_push_subscriptions.sql — one row per browser/device, keyed to users.id (ON DELETE CASCADE).
  • Config flags (PUSH_ENABLED, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT), Prometheus counters (tensies_push_sent_total, …_failed_total, …_pruned_total), and a root-scope /sw.js route in main.py.

Frontend (minimal opt-in, no UI button)

  • static/sw.js — service worker: push + notificationclick handlers.
  • static/js/push.js — opt-in (permission → subscribe → save); fired best-effort on load, no-ops unless signed in and push is enabled server-side.

Tooling / docs

  • scripts/gen_vapid.py — self-validating VAPID key generation.
  • docs/PUSH.md, plus compose + .env example wiring.

Verification (docker-compose stack)

  • Migration 008 applies; /sw.js registers at root scope in a real browser at mobile viewport (390×844).
  • /push/subscribe stores a row (401 without JWT); send_to_user produced a 165-byte encrypted POST to a mock push service (delivered=1) and pruned the subscription on a 410.
  • CLI exit codes correct (disabled→2, unknown user→1, no-subs→1); username→UUID resolves.
  • push.js is type-clean (0 tsc errors); app boots with no real console errors and the opt-in correctly no-ops when signed out.

Notes

  • Image build: deps install from requirements.lock (pinned pywebpush, py-vapid, http-ece). In the sandbox a clean docker build fails only because http-ece is sdist-only and pip's build isolation can't fetch setuptools through the TLS-intercepting proxy — an environment quirk, not a prod issue. Pinned versions resolve and were installed into the running container to complete testing.
  • iOS delivers web push only when the PWA is installed to the home screen (documented in docs/PUSH.md).

🤖 Generated with Claude Code


Generated by Claude Code

Stand up the framework for browser push notifications, end to end:
a signed-in account can subscribe and a notification can be sent to any
account by UUID or username from a script. No game-event triggers yet —
those are an additive next step.

Backend:
- server/push.py: the single send path (send_to_user) + subscription
  storage; pywebpush encryption runs in a thread, dead subs (404/410)
  are pruned, failures are metered and never raised.
- server/push_routes.py: GET /push/vapid-public-key, JWT-authed
  POST /push/subscribe + /push/unsubscribe (shared require_user dep
  added to server/auth.py).
- migrations/008_push_subscriptions.sql: one row per device, keyed to
  users.id (ON DELETE CASCADE).
- config flags (PUSH_ENABLED + VAPID_*), Prometheus counters, and the
  /sw.js root-scope route wired in main.py.

Frontend:
- static/sw.js: service worker (push + notificationclick).
- static/js/push.js: opt-in (permission -> subscribe -> save); fired
  best-effort on load, no-ops unless signed in and push is enabled.

Tooling/docs:
- scripts/gen_vapid.py (self-validating key generation),
  scripts/send_push.py (send to a user), docs/PUSH.md, compose + env
  wiring (off by default).

Verified against the docker-compose stack: migration applies, /sw.js
registers at root scope, the subscribe endpoint stores a row, send_to_user
encrypts + POSTs to the push service (delivered=1) and prunes on 410, and
the CLI resolves username->UUID with correct exit codes. App loads clean
at mobile viewport (390x844) with the push module imported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177rY2Jfvn9po32avwRvfFB
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