Skip to content

Implement push notifications and the notification inbox - #12

Merged
Adron merged 9 commits into
mainfrom
dev
Oct 21, 2026
Merged

Adron merged 9 commits into
mainfrom
dev

Conversation

@Adron

@Adron Adron commented Oct 19, 2026

Copy link
Copy Markdown
Member

Release notes

Push notifications (API Reference → Push Notifications) are built. All 17 operations work, and the 501 not_implemented stubs are gone. This is Phase 2 item 9 in PLAN.md.

What's in it

  • internal/push: provider clients, with no database access.
    • APNs over HTTP/2 with ES256 provider tokens, cached for 50 minutes.
    • FCM HTTP v1 with service-account OAuth, with the access token cached.
    • Web Push with VAPID JWTs and RFC 8291 aes128gcm encryption. The encryption is verified byte for byte against the RFC's test vector.
    • Every provider answer is classified as sent, retry, token gone, credential rejected, or failed.
    • It also includes a recording fake for tests and a log backend for local runs.
  • Migration 0003_push.sql (expand-only):
    • four tables, three of them under test/live row-level security;
    • applications.receives_platform_notifications;
    • notifications.send for superadmin;
    • four new audit actions.
  • Endpoints:
    • Push credentials: managed by people only. Key material is write-only and KMS-encrypted, and VAPID keys are generated.
    • Devices: a token-keyed upsert, bound to the registering session, capped at 20 active per User per app. A token registered by another User moves to them, and the old device is deleted.
    • Send: an access check, the push preference, 60 per recipient per hour, the 4 KB payload check, and deliveries written in the same transaction.
    • Inbox: list, get with deliveries, state changes, mark-read, and unread counts.
  • Delivery worker:
    • Deliveries are claimed with a lease, so no transaction stays open across a provider call.
    • Retries run within the TTL.
    • A gone token deactivates the device; a rejected credential is marked failed, its owner is emailed, and push_credential.failed fires.
    • Each notification's push summary is recomputed, firing notification.push_failed when nothing went out.
  • Platform alerts: new sign-in, password and email changes, MFA disabled, refresh-token reuse, organization invitation, and developer-granted access. Each is raised before any session revocation in the same transaction, so the warning reaches the devices about to be signed out.
  • Lifecycle:
    • Devices deactivate on every session revocation (logout, single revoke, password reset or change, token reuse, MFA reset) and on suspension or deletion.
    • Export, the erasure cascade, and the test-data purge cover the new data.
    • A daily push-maintenance job handles stale devices and inbox retention.
  • Operations:
    • PUSH_BACKEND is live (production default) or log (local default).
    • Push shares the webhook SQS queue, worker Lambda, and 1-minute sweep, so there's no new infrastructure.
    • Terraform adds only the push-maintenance schedule.
  • Docs:
    • The implementation-status notes are removed.
    • The schema now records the push-count columns.
    • The DEPLOYMENT, README, PLAN, and Changelog entries are updated.

No API shape changed, so openapi.yaml only loses the "returns 501" note on the Notifications tag.

Verification

  • make check passes locally. That covers lint, gen-check, and every unit, integration, and contract test, including the new push suites. Every response in them is validated against openapi.yaml. It also covers the Lambda builds and terraform fmt.
  • internal/push has provider protocol tests:
    • APNs against a real HTTP/2 TLS server, with ES256 signature verification;
    • FCM against a fake OAuth exchange and send;
    • Web Push against a fake push service that decrypts the payload;
    • the RFC 8291 test vector.
  • The Quickstart passed against a local devserver, and a manual push flow on the devserver settled to sent through the in-process worker.
  • staticcheck with CI's check set is clean on new code.
  • The docs build, and every internal link resolves.

Before turning on live in production: each Application needs its own APNs, FCM, or Web Push credential configured. Until one exists, sends for that app settle as skipped: no_credentials.

🤖 Generated with Claude Code

Adron and others added 6 commits October 18, 2026 17:46
Replaces the 501 stubs with the full feature (API Reference → Push
Notifications), Phase 2 item 9:

- internal/push: APNs (HTTP/2, ES256 provider tokens), FCM HTTP v1
  (service-account OAuth), and Web Push (VAPID, RFC 8291 aes128gcm, checked
  against the RFC's test vector), with cached provider auth, outcome
  classification, a recording fake, and a log backend
- migration 0003: push_credentials, devices, notifications,
  notification_deliveries (test/live RLS), applications.
  receives_platform_notifications, notifications.send for superadmin,
  new audit actions
- push credentials (people only, write-only KMS-encrypted key material,
  generated VAPID keys), device registration (token-keyed upsert, session
  binding, 20-device cap, token moves between Users), send (access check,
  push preference, per-recipient limit, 4 KB payload check, deliveries in
  the same transaction), and the inbox
- delivery worker with leases, retries within the TTL, device deactivation
  on gone tokens, credential failure handling, and push summaries;
  notification.push_failed / device.* / push_credential.failed webhooks
- platform alerts (new sign-in, password and email changes, MFA disabled,
  token reuse, organization invitation, developer-granted access), raised
  before any session revocation
- devices deactivate on every session revocation, suspension, and deletion;
  export, erasure cascade, test-data purge, and a daily push-maintenance job
- PUSH_BACKEND (live | log); push shares the webhook queue, worker, and sweep
- integration tests for every endpoint and outcome, provider protocol tests
- docs updated: implementation status, schema counters, DEPLOYMENT, README,
  PLAN, Changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first production deploy failed with 'Not authorized to perform
sts:AssumeRoleWithWebIdentity': this repository issues immutable subject
claims (repo:CompositeCode@38675098/substratalapps.com@1403762290:...),
and the plan and deploy roles trusted the name-only form. The subject is
now a variable defaulting to the immutable form, which also means a
renamed or recreated repository can't inherit the roles. Already applied.

Also ignore .claude/settings.local.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first production apply failed: 'Cannot find version 16.6 for
aurora-postgresql'. 16.6 has been retired; 16.15 is the newest 16.x,
keeping the major version CI and local development test against
(postgres:16), and supports the Data API on Serverless v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Everything in them is either built and documented where it belongs, or
consolidated into plan-remaining.md.

- infra/README.md: DEPLOYMENT.md's as-built content, next to the Terraform
  that implements it, plus the real account setup (Projects Organization,
  the SCP exception for substratal-prod, aws login). The unbuilt sections
  (growth trajectory, scale-out, MCP scaling, dedicated tenancy
  provisioning and its migration runbook) moved out; the finished build
  checklist was dropped.
- plan-remaining.md: test-mode User expiry (its migration renumbered to
  0005, since 0004 is the protected superadmin), the first-party web pages
  (dashboard, hosted sign-in), Phase 3, dedicated tenancy infrastructure,
  growth trajectory, scale-out, MCP scaling, production verification of
  the phase exit criteria, and the rules for every change.
- remaining_manual_steps.md: the one-time steps left to go live (Lambda
  quota, redeploy, superadmin password, alert emails, SES production
  access, Stripe, push credentials, registering Applications, the plan-job
  fix, legal, the optional domain transfer).
- ORIGINAL-SPEC-DRAFT.md: fully superseded by the docs site; deleted.
- Every link and comment repointed: docs site, README, Terraform, Go.
  Historical Changelog links point at the last commit that had the files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retire PLAN.md, DEPLOYMENT.md, ORIGINAL-SPEC-DRAFT.md; add plan-remaining.md and remaining_manual_steps.md
@Adron
Adron merged commit 3e6327f into main Oct 21, 2026
7 of 8 checks passed
@Adron Adron mentioned this pull request Oct 21, 2026
1 of 2 tasks
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