This FRD ships @dmwd-io/notifications as an optional package that re-exports zod (interface only) as the platform standard for in-app notification delivery — coordinating with @dmwd-io/email, @dmwd-io/push, and @dmwd-io/sms for cross-channel consistency. The value is platform-wide standards and drift prevention, not a custom abstraction layer.
Introduction
Overview
The design system ships three notification UI components — notification-bell, notification-feed, and notification-card — but there is no shared data model behind them. Each consuming app defines its own notification shape, fetches from its own API, and implements read/unread tracking independently. Per ADR-014 (Open Source First), this FRD designates zod (interface only) as the community library for notification schema validation, re-exports it through @dmwd-io/notifications, and documents the platform env var conventions. No custom NotificationsProvider interface is built — the library’s own API is the interface.
Goals
Designate zod (interface only) as the platform standard for notification schema validation.
Re-export zod (interface only) via @dmwd-io/notifications so all apps pull from one canonical source.
Document platform env var conventions for notification channel configuration (see channel-specific env vars).
Provide a canonical Notification Zod schema used by all notification UI components.
Integrate with the existing notification-bell (unread count badge) and notification-feed (scrollable list) widgets.
Non-Goals
Building push notification infrastructure (FCM, APNs, Web Push).
Implementing email or SMS notification channels — this provider is for in-app notifications only.
Building new notification UI components beyond integrating with existing widgets.
Implementing notification routing or fan-out logic (backend concern).
Notification template rendering or content management.
Building a custom provider interface or adapter layer — the library’s own API is the interface (per ADR-014).
Scope
In Scope
Area
Description
Re-export package
@dmwd-io/notifications re-exporting zod (interface only) as the platform standard
Notification schema
Notification Zod schema with id, type, title, body, read status, timestamp, action URL, and metadata
Preference model
NotificationPreferences Zod schema for per-category opt-in/opt-out and delivery channel settings
Env var conventions
Platform-standard env var names for notification channel configuration
Widget integration
Documentation and examples wiring notification-bell and notification-feed to the schema
Out of Scope
Area
Reason
Custom provider interface
Per ADR-014, the library’s API is the interface
Custom adapter factory
Per ADR-014, apps use the library directly
Vendor adapter implementations
Separate packages per ADR-051
Push notifications (FCM, APNs, Web Push)
Infrastructure concern, not a library contract
Email/SMS notification channels
Covered by @dmwd-io/email; out-of-band channels are separate
Notification content authoring/templates
Backend content management concern
Notification grouping/threading
Future extension; initial scope is flat notification list
Users and Pain Points
User Groups
User
Description
Needs
App developers
Engineers building notification features
A single typed schema for notification data with platform-standard validation
QA engineers
Testers validating notification flows
Deterministic mock data for read, unread, and dismissed states
Design system maintainers
Library contributors
A data contract that the existing notification widgets can consume directly
Pain Points
User
Pain Point
Impact
App developers
Each app defines its own notification shape; notification-card props vary per app
Widgets cannot be used without app-specific data transformation
App developers
No shared schema means notification widgets show static data in Storybook
Cannot demo real-time notification arrival or read/unread transitions
App developers
Unread count logic is reimplemented in every app
Inconsistent badge behavior across apps
Definitions
Term
Definition
Notification
An in-app message delivered to a user, with a type, title, body, and optional action URL
Unread
A notification that has not been explicitly marked as read by the user
Dismissed
A notification the user has removed from their feed (soft delete)
Notification preference
A per-category opt-in/opt-out setting controlling which notifications a user receives
Subscription
A real-time listener that receives new notifications as they arrive
Current State
Existing Behavior
notification-bell.tsx renders an icon with an unread count badge. It accepts unreadCount as a prop. notification-feed.tsx renders a scrollable list of notification-card components. Each card accepts title, body, timestamp, and isRead props. There is no shared data model — each app maps its own API response to these props.
Current Limitations
No shared Notification TypeScript type — each app defines its own.
No read/unread state management contract — apps track read state in their own stores.
No real-time delivery abstraction — apps wire their own WebSocket or SSE connections.
No preference management — users cannot opt out of notification categories.
notification-bell unread count must be computed and passed in by the app.
Existing Workarounds
Apps fetch notifications from their own APIs and transform the response before passing to widgets.
Storybook stories hard-code 2-3 notification objects with static read/unread state.
Unread count is computed in the app and passed as a number prop to the bell widget.
Database Schema
The notifications provider requires a notifications table in PostgreSQL:
-- notifications table
CREATETABLEnotifications (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
tenant_id UUID NOT NULLREFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULLREFERENCES users(id) ON DELETE CASCADE,
typeTEXTNOT NULL, -- e.g. "mention", "assignment", "billing.invoice_paid"
title TEXTNOT NULL,
body TEXT,
action_url TEXT,
read_at TIMESTAMPTZ, -- NULL = unread
dismissed_at TIMESTAMPTZ,
metadata JSONB DEFAULT'{}',
created_at TIMESTAMPTZNOT NULLDEFAULTNOW(),
updated_at TIMESTAMPTZNOT NULLDEFAULTNOW()
);
CREATEINDEXidx_notifications_user_unread
ON notifications (user_id, read_at)
WHERE read_at ISNULL;
CREATEINDEXidx_notifications_tenant_user
ON notifications (tenant_id, user_id, created_at DESC);
NotificationPreferences table
CREATETABLEnotification_preferences (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
user_id UUID NOT NULLREFERENCES users(id) ON DELETE CASCADE,
category TEXTNOT NULL, -- e.g. "billing", "mentions", "system"
in_app BOOLEANNOT NULLDEFAULT TRUE,
email BOOLEANNOT NULLDEFAULT TRUE,
push BOOLEANNOT NULLDEFAULT FALSE,
updated_at TIMESTAMPTZNOT NULLDEFAULTNOW(),
UNIQUE (user_id, category)
);
Migration tool: Atlas (preferred) or Drizzle Kit per ADR-027 §4.
Proposed Solution
Summary
Per ADR-014, @dmwd-io/notifications is a thin re-export package. It re-exports zod (interface only) as the platform standard and documents the env var conventions for notification channel configuration. No custom interface is built — the library’s API is the interface.
Environment variable conventions follow the channel-specific env var names documented in the package README. No custom adapter or factory function is provided.
Key Capabilities
Zod schemas for Notification and NotificationPreferences validated at runtime.
Re-export of zod (interface only) so all apps share one version and one import path.
Platform env var documentation for in-app, email, push, and SMS channel configuration.
TypeScript types inferred from Zod schemas — no separate type declarations needed.
User Experience
End users see consistent notification behavior across apps — same badge counting, same read/unread semantics, same feed interaction patterns — because all apps validate against the same schema.
Developer Experience
Developers import from @dmwd-io/notifications and get the canonical Zod schema. The notification-bell reads unreadCount from their own data layer. The notification-feed calls their own API and validates the response against the shared schema. No factory function or context provider is needed.
Requirements
ID
Requirement
Priority
Notes
FR-001
Re-export zod (interface only) from @dmwd-io/notifications
Must
Per ADR-014
FR-002
Export a canonical NotificationSchema Zod schema with id, type, title, body, read, timestamp, actionUrl, and metadata
Must
-
FR-003
Export NotificationPreferencesSchema Zod schema with per-category settings
Should
-
FR-004
Export TypeScript types inferred from Zod schemas
Must
-
FR-005
Document platform env var conventions in package README
Implement push notification infrastructure (FCM, APNs, Web Push).
Decision Log
Date
Decision
Reason
Owner
2026-05-26
Use ISO 8601 strings for timestamps instead of Date objects
Serialization safety across JSON boundaries
David Holmes
2026-05-26
Notification.type is a free string, not a closed union
Apps define their own notification categories; the provider should not constrain them
David Holmes
2026-05-26
dismiss is a soft delete, not permanent removal
Users may want to recover dismissed notifications; permanent delete is a future extension
David Holmes
2026-06-02
Reframed per ADR-014 (Open Source First): adopt zod (interface only) as thin re-export rather than building a custom provider interface. Library API is the interface.
ADR-014 mandates open source first; no custom abstraction is needed when the library’s types and schemas serve the same purpose
David Holmes
Document History
Date
Author
Change
2026-05-26
David Holmes
Initial draft
2026-06-02
David Holmes
Reframed per ADR-014: ship as thin re-export of zod (interface only), drop custom interface.