Skip to content

FRD: Push Provider

Document Summary

FieldDetails
Feature NamePush Provider Library
StatusDraft
OwnerDavid Holmes
ContributorsEngineering
Target Releasev2.0.0 (P2)
Related LinksRoadmap item #73, ADR-027 (Default Tech Stack), ADR-014 (Open Source First)
Last Updated2026-06-02
Open Source Librariesweb-push
Documentationweb-push README · Web Push API (MDN) · VAPID Explainer

This FRD ships @dmwd-io/push as an optional package that re-exports web-push as the platform standard for Web Push notifications. The value is consistency and drift prevention, not a custom abstraction — per ADR-014, we adopt the community library directly rather than building our own interface on top of it.

Introduction

Overview

Per ADR-014 (Open Source First), this package designates web-push as the platform standard for browser push notifications. @dmwd-io/push is a thin re-export of web-push with documented platform conventions: use VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT as the canonical env var names across all services. No custom PushProvider interface is built — the web-push library’s own API is the interface.

Goals

  • Designate web-push as the platform standard for Web Push (VAPID-based browser push).
  • Re-export web-push through @dmwd-io/push so all services import from a single platform-owned entry point.
  • Document VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT as the canonical platform env var names.
  • Prevent drift by giving teams one place to update if the underlying library ever changes.
  • Model push subscriptions as a typed, serializable data structure for persistence.
  • Define a failure-handling strategy with typed error categories (expired token, invalid payload, rate-limited, service unavailable).
  • Ship a mock adapter for testing that records dispatched notifications and simulates failure scenarios.

Non-Goals

  • Building a custom PushProvider interface or adapter layer — the library’s own API is the interface (per ADR-014).
  • Building production FCM or APNs (mobile) adapters (browser Web Push via web-push is in scope per ADR-027’s self-hosted preference).
  • Building a notification preferences UI or user-facing settings page.
  • Managing push notification content scheduling or batching at the infrastructure level.
  • Handling in-app notification rendering (that is a UI component concern).

Scope

In Scope

AreaDescription
Re-export package@dmwd-io/push re-exports all of web-push; no custom interface layer
Env var conventionsDocument VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT as platform-standard names
Subscription modelTyped structure for device tokens, platform identifiers, and subscription metadata
Notification payloadTyped structure for title, body, data payload, badge count, and action URLs
Delivery resultDiscriminated union covering delivered, failed, expired, rate-limited
Failure handlingTyped error categories with recommended caller behavior per category
Mock adapterIn-memory provider for testing with configurable failure simulation
Unit testsFull contract conformance and mock behavior coverage
DocumentationStorybook MDX docs with usage examples

Out of Scope

AreaReason
Custom PushProvider interfaceADR-014: community library API is the interface
FCM / APNs adaptersVendor-specific; shipped separately
Service worker registrationClient-side concern outside library scope
Notification scheduling / queuingApplication infrastructure concern
User preference managementUI and persistence layer concern
Rich media attachmentsDeferred to a future iteration

Users and Pain Points

User Groups

UserDescriptionNeeds
Application developersEngineers adding push notification featuresA stable interface to code against without vendor coupling
QA engineersTesters verifying notification-dependent flowsA mock that lets them assert on dispatched notifications
Platform engineersEngineers building production adaptersA clear contract and conformance tests

Pain Points

UserPain PointImpact
Application developersEach service has its own push SDK integration with different error handlingInconsistent behavior; duplicated initialization code
QA engineersCannot verify push content in automated tests without real device tokensTests skip push verification or rely on manual QA
Application developersToken expiration and invalid-token errors are handled differently per serviceStale subscriptions accumulate; users stop receiving notifications

Definitions

TermDefinition
Push providerAn object implementing the PushProvider interface that dispatches notifications through a specific service
SubscriptionA record associating a device token with a platform and optional metadata (user ID, topic)
Notification payloadThe content of a push notification: title, body, optional data, optional action URL
Expired tokenA device token that the push service reports as no longer valid
Mock adapterAn in-memory PushProvider that records notifications for test assertions

Current State

Existing Behavior

No shared push notification abstraction exists. Services that send push notifications integrate directly with vendor SDKs.

Current Limitations

  • No shared subscription model; each service stores tokens differently.
  • No typed delivery result; services parse vendor responses ad hoc.
  • No strategy for handling expired tokens; stale subscriptions persist.
  • No mock adapter; push-dependent flows are untestable in isolation.

Existing Workarounds

  • Developers wrap vendor SDKs in service-local helper functions with inconsistent signatures.
  • Tests mock HTTP calls at the transport level, tightly coupling test code to vendor API shapes.

Database Schema

Push subscription persistence requires a push_subscriptions table:

CREATE TABLE push_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL UNIQUE,
p256dh_key TEXT NOT NULL, -- public key for payload encryption
auth_key TEXT NOT NULL, -- auth secret
platform TEXT NOT NULL DEFAULT 'web', -- "web" | "fcm" | "apns"
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ
);
CREATE INDEX idx_push_subscriptions_user
ON push_subscriptions (user_id);

VAPID keys (required for Web Push) are stored in 1Password per ADR-072, not in the database.


Proposed Solution

Summary

Per ADR-014, @dmwd-io/push is a thin re-export of web-push. No custom interface is built — the library’s API is the interface. The package’s value is a single platform-owned import path and documented env var conventions.

// All web-push exports are available through the platform package
import { sendNotification, generateVAPIDKeys } from '@dmwd-io/push';

Platform env var conventions:

Env VarDescription
VAPID_PUBLIC_KEYVAPID public key for Web Push authentication
VAPID_PRIVATE_KEYVAPID private key (stored in 1Password per ADR-072)
VAPID_SUBJECTVAPID subject (mailto: or URL identifying the sender)

Key Capabilities

  • @dmwd-io/push re-exports all of web-push from a single platform entry point.
  • Documented env var names (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT) used consistently across all services.
  • PushSubscription type modeling device token, platform, and metadata for persistence.
  • PushNotification type for payload with title, body, data, badge, and action URL.
  • PushDeliveryResult discriminated union with delivered, failed, expired, rate-limited statuses.
  • MockPushProvider with configurable failure simulation and assertion helpers.

User Experience

Not applicable (library, no UI).

Developer Experience

Developers import from @dmwd-io/push using the web-push API directly. VAPID initialization reads from the documented env vars. In tests, MockPushProvider records all dispatched notifications and can be configured to simulate specific failure modes (expired tokens, rate limits).


Requirements

IDRequirementPriorityNotes
FR-001The package must re-export all of web-push from @dmwd-io/pushMustADR-014: library API is the interface
FR-002The package must export a typed PushSubscription modelMustToken, platform, metadata
FR-003The package must export a PushDeliveryResult discriminated unionMustCovers all outcome states
FR-004The package must ship a MockPushProvider adapterMustFor testing
FR-005The package must document recommended caller behavior for each failure categoryMustPart of the contract
FR-006The mock adapter should support configurable failure simulationShouldFor testing error paths

Priority Definitions

PriorityMeaning
MustRequired for this feature to ship.
ShouldImportant, but can be deferred if needed.
CouldNice to have. Not required for initial release.

Functional Requirements

IDRequirementUser BenefitPriority
FUNC-001subscribe(subscription) stores a subscription and returns a confirmation resultUniform subscription managementMust
FUNC-002unsubscribe(token) removes a subscription by device tokenClean token lifecycleMust
FUNC-003send(token, notification) dispatches a notification to a single device and returns PushDeliveryResultUniform send interfaceMust
FUNC-004sendBatch(messages) dispatches to multiple tokens and returns ordered resultsBulk notification supportShould
FUNC-005PushDeliveryResult includes status, token, timestamp, and optional error with errorCategoryCallers can route error handling by categoryMust
FUNC-006When result status is expired, the caller should remove or refresh the subscriptionPrevents stale token accumulationMust
FUNC-007MockPushProvider.getDispatched() returns all sent notificationsTest assertionsMust
FUNC-008MockPushProvider.simulateFailure(token, status) configures a token to return a specific failureTesting error handling pathsShould

Non-Functional Requirements

IDRequirementCategoryPriority
NFR-001web-push is the only runtime dependencyMaintainabilityMust
NFR-002All public types exported from the package entry pointMaintainabilityMust
NFR-003Mock adapter instances must not share mutable stateTestingMust
NFR-004The library must not persist or log device tokens by defaultSecurityMust
NFR-005Bundle size under 5 KB minified + gzipped (excluding web-push peer dep)PerformanceShould
NFR-006Tree-shakeable exportsPerformanceShould

API / Interface Requirements

Public API

NameTypeDescriptionRequired
PushSubscriptiontype{ token: string; platform: 'ios' | 'android' | 'web'; userId?: string; topics?: string[]; metadata?: Record<string, string> }Yes
PushNotificationtype{ title: string; body: string; data?: Record<string, unknown>; badge?: number; actionUrl?: string }Yes
PushDeliveryResulttype{ status: 'delivered' | 'failed' | 'expired' | 'rate-limited'; token: string; timestamp: number; error?: string; errorCategory?: string }Yes
MockPushProviderclassIn-memory provider with assertion and simulation helpersYes
web-push re-exportsallAll named exports from web-push (sendNotification, generateVAPIDKeys, etc.)Yes

Example Usage

import { sendNotification, generateVAPIDKeys } from '@dmwd-io/push';
// Initialize VAPID using platform-standard env var names
const vapidKeys = {
publicKey: process.env.VAPID_PUBLIC_KEY!,
privateKey: process.env.VAPID_PRIVATE_KEY!,
subject: process.env.VAPID_SUBJECT!,
};
// Send a notification using the web-push API directly
await sendNotification(subscription, JSON.stringify({ title: 'New message' }), {
vapidDetails: vapidKeys,
});
import { MockPushProvider } from '@dmwd-io/push';
// Test
const mock = new MockPushProvider();
await mock.send('device-token-123', { title: 'New message', body: 'You have a new message waiting.' });
const dispatched = mock.getDispatched();
// dispatched[0].notification.title === "New message"

API Notes

  • No custom PushProvider interface is defined — web-push’s own API is the interface per ADR-014.
  • MockPushProvider is provided for test isolation and does not wrap web-push internals.
  • The mock adapter defaults to status: 'delivered' unless a failure is configured.
  • Platform-specific payload extensions (iOS sound, Android channel) use the data field.

Accessibility Requirements

IDRequirementNotes
A11Y-001Not directly applicable; headless libraryNo UI surface

Checklist

  • Keyboard support is defined. (N/A — no UI)
  • Focus behavior is defined. (N/A — no UI)
  • Screen reader behavior is defined. (N/A — no UI)
  • Color contrast requirements are met. (N/A — no UI)
  • Reduced motion behavior is considered. (N/A — no UI)
  • Semantic HTML expectations are documented. (N/A — no UI)
  • ARIA usage is defined only where needed. (N/A — no UI)

Content and Documentation Requirements

IDRequirementLocationPriority
DOC-001API reference with all exported typesStorybook MDXMust
DOC-002Usage guide showing VAPID initialization with platform env vars and subscription lifecycleStorybook MDXMust
DOC-003Testing guide showing MockPushProvider with failure simulationStorybook MDXMust
DOC-004Error handling guide with recommended actions per failure categoryStorybook MDXMust
DOC-005Migration guide for services moving from direct web-push imports to @dmwd-io/pushStorybook MDXShould

Documentation Should Include

  • Overview of the re-export pattern and why it prevents drift (ADR-014)
  • When to use the library vs. direct SDK calls
  • Installation and import
  • VAPID initialization using platform env var names
  • Subscription lifecycle (register, refresh, remove)
  • Sending notifications (single and batch)
  • Error handling by category
  • Testing with MockPushProvider

Dependencies

DependencyTypeOwnerStatusNotes
TypeScript 5.xEngineeringDavid HolmesReadyBuild toolchain
VitestEngineeringDavid HolmesReadyTest runner
web-pushRuntime (peer)CommunityReadyPlatform-standard Web Push library per ADR-014

Risks and Tradeoffs

Risk / TradeoffImpactMitigation
Generic contract may not cover all vendor capabilities (e.g., iOS-specific priority, Android channels)Some vendor features require adapter-specific configurationUse the data field for platform-specific extensions; document this pattern
Subscription model may not capture all real-world needsSome apps need topic-based subscriptions, others need user-basedInclude both topics and userId as optional fields; keep the model extensible
Expired-token handling is advisory, not enforcedCallers may ignore expired results and accumulate stale tokensDocument the recommended pattern clearly; provide a utility function for cleanup
No built-in retry or exponential backoffCallers must handle retry themselvesLibrary stays focused on contract; retry is an application concern
Thin re-export adds a layer without adding behaviorDevelopers may bypass @dmwd-io/push and import web-push directlyLint rule or import boundary check can enforce the platform entry point

Open Questions

IDQuestionOwnerStatusResolution
Q-001Should sendBatch support partial failure (some delivered, some failed) or fail atomically?David HolmesOpen
Q-002Should the subscription model include a createdAt timestamp for token freshness tracking?David HolmesOpen
Q-003Should we define a PushProviderConfig type for adapter initialization options?David HolmesOpen

Acceptance Criteria

IDCriteriaRelated Requirement
AC-001@dmwd-io/push re-exports all named exports from web-pushFR-001
AC-002PushSubscription type includes token, platform, and optional userId and topicsFR-002
AC-003PushDeliveryResult covers delivered, failed, expired, rate-limited statusesFR-003
AC-004MockPushProvider.getDispatched() returns all sent notificationsFUNC-007
AC-005MockPushProvider.simulateFailure() causes specified tokens to return configured failure statusFUNC-008
AC-006All public types re-exported from package indexNFR-002
AC-007Unit tests pass covering subscribe, unsubscribe, send, batch send, and all failure categoriesFR-004
AC-008Storybook MDX docs render without errorsDOC-001
AC-009Package readme documents VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT as canonical env var namesFR-001

LLM Handoff Instructions

Expected LLM Behavior

  • Follow the requirements and acceptance criteria in this document.
  • Do not expand scope beyond the In Scope section.
  • Respect the Out of Scope section.
  • Ask for clarification only when a requirement cannot be safely interpreted.
  • Prefer existing design system patterns over inventing new ones.
  • Use discriminated unions for result types, not string enums.
  • The mock adapter must be a class, not a factory function.
  • Document recommended caller behavior for each PushDeliveryResult status in code comments.
  • Do NOT build a custom PushProvider interface — per ADR-014, the library’s own API is the interface.

Implementation Steps

  1. Create packages/push/ directory if it does not exist.
  2. Add package.json with web-push as a peer dependency and @dmwd-io/push as the package name.
  3. Create src/index.ts with export * from 'web-push' plus the platform-specific types (PushSubscription, PushNotification, PushDeliveryResult) and MockPushProvider.
  4. Document env var names (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT) in the package README and in JSDoc on any initialization helpers.
  5. Run pnpm typecheck and confirm no type errors before declaring complete.

LLM Should Not

  • Invent undocumented product behavior.
  • Connect to real push services.
  • Build a custom PushProvider interface wrapping web-push.
  • Add new dependencies without justification.
  • Change unrelated components.
  • Include real device tokens in test fixtures.
  • Implement scheduling or queue logic inside the provider contract.

Decision Log

DateDecisionReasonOwner
2026-05-26Discriminated union for delivery resultsType narrowing for safe error handlingDavid Holmes
2026-05-26Include topics and userId in subscription modelSupport both topic-based and user-based subscription patternsDavid Holmes
2026-05-26Advisory expired-token handling (not enforced)Library should not manage subscription storage; that is the consumer’s responsibilityDavid Holmes
2026-06-02Reframed per ADR-014 (Open Source First): adopt web-push as thin re-export rather than building a custom provider interface. Library API is the interface.ADR-014 mandates community library adoption over custom abstractions when the library covers the use caseDavid Holmes

Document History

DateAuthorChange
2026-05-26David HolmesInitial draft
2026-06-02David HolmesReframed per ADR-014: ship as thin re-export of web-push, drop custom interface.