unified-notification-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js ADDED
@@ -0,0 +1,149 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { boolean, check, index, integer, jsonb, pgTableCreator, primaryKey, text, timestamp, uniqueIndex, uuid, varchar, } from "drizzle-orm/pg-core";
3
+ function validatePrefix(prefix) {
4
+ if (!/^[a-z_][a-z0-9_]*$/i.test(prefix)) {
5
+ throw new Error("tablePrefix must start with a letter or underscore and contain only letters, numbers, and underscores");
6
+ }
7
+ if (prefix.length > 23) {
8
+ throw new Error("tablePrefix must be at most 23 characters");
9
+ }
10
+ }
11
+ export function createNotificationSchema(options = {}) {
12
+ const prefix = options.tablePrefix ?? "unc_";
13
+ validatePrefix(prefix);
14
+ const table = pgTableCreator((name) => `${prefix}${name}`);
15
+ const name = (suffix) => `${prefix}${suffix}`;
16
+ const notifications = table("notifications", {
17
+ id: uuid("id").primaryKey().defaultRandom(),
18
+ recipientId: text("recipient_id").notNull(),
19
+ topic: varchar("topic", { length: 191 }).notNull(),
20
+ title: text("title").notNull(),
21
+ body: text("body").notNull(),
22
+ actionUrl: text("action_url"),
23
+ data: jsonb("data").$type().notNull().default({}),
24
+ payload: jsonb("payload")
25
+ .$type()
26
+ .notNull(),
27
+ actorId: text("actor_id"),
28
+ entityType: varchar("entity_type", { length: 191 }),
29
+ entityId: text("entity_id"),
30
+ idempotencyKey: varchar("idempotency_key", { length: 255 }),
31
+ scheduledFor: timestamp("scheduled_for", {
32
+ withTimezone: true,
33
+ mode: "date",
34
+ })
35
+ .notNull()
36
+ .defaultNow(),
37
+ readAt: timestamp("read_at", { withTimezone: true, mode: "date" }),
38
+ archivedAt: timestamp("archived_at", {
39
+ withTimezone: true,
40
+ mode: "date",
41
+ }),
42
+ canceledAt: timestamp("canceled_at", {
43
+ withTimezone: true,
44
+ mode: "date",
45
+ }),
46
+ createdAt: timestamp("created_at", {
47
+ withTimezone: true,
48
+ mode: "date",
49
+ })
50
+ .notNull()
51
+ .defaultNow(),
52
+ updatedAt: timestamp("updated_at", {
53
+ withTimezone: true,
54
+ mode: "date",
55
+ })
56
+ .notNull()
57
+ .defaultNow(),
58
+ }, (columns) => [
59
+ index(name("notifications_recipient_created_idx")).on(columns.recipientId, columns.createdAt.desc()),
60
+ index(name("notifications_recipient_unread_idx"))
61
+ .on(columns.recipientId, columns.createdAt.desc())
62
+ .where(sql `${columns.readAt} is null and ${columns.archivedAt} is null`),
63
+ uniqueIndex(name("notifications_recipient_idempotency_uidx"))
64
+ .on(columns.recipientId, columns.idempotencyKey)
65
+ .where(sql `${columns.idempotencyKey} is not null`),
66
+ ]);
67
+ const deliveries = table("deliveries", {
68
+ id: uuid("id").primaryKey().defaultRandom(),
69
+ notificationId: uuid("notification_id")
70
+ .notNull()
71
+ .references(() => notifications.id, { onDelete: "cascade" }),
72
+ channel: varchar("channel", { length: 64 }).notNull(),
73
+ status: varchar("status", { length: 20 })
74
+ .$type()
75
+ .notNull()
76
+ .default("pending"),
77
+ attempts: integer("attempts").notNull().default(0),
78
+ maxAttempts: integer("max_attempts").notNull().default(5),
79
+ nextAttemptAt: timestamp("next_attempt_at", {
80
+ withTimezone: true,
81
+ mode: "date",
82
+ }),
83
+ lastAttemptAt: timestamp("last_attempt_at", {
84
+ withTimezone: true,
85
+ mode: "date",
86
+ }),
87
+ lockedAt: timestamp("locked_at", {
88
+ withTimezone: true,
89
+ mode: "date",
90
+ }),
91
+ lockedBy: varchar("locked_by", { length: 191 }),
92
+ sentAt: timestamp("sent_at", { withTimezone: true, mode: "date" }),
93
+ providerMessageId: text("provider_message_id"),
94
+ errorCode: varchar("error_code", { length: 191 }),
95
+ errorMessage: text("error_message"),
96
+ metadata: jsonb("metadata").$type(),
97
+ createdAt: timestamp("created_at", {
98
+ withTimezone: true,
99
+ mode: "date",
100
+ })
101
+ .notNull()
102
+ .defaultNow(),
103
+ updatedAt: timestamp("updated_at", {
104
+ withTimezone: true,
105
+ mode: "date",
106
+ })
107
+ .notNull()
108
+ .defaultNow(),
109
+ }, (columns) => [
110
+ uniqueIndex(name("deliveries_notification_channel_uidx")).on(columns.notificationId, columns.channel),
111
+ index(name("deliveries_due_idx"))
112
+ .on(columns.nextAttemptAt, columns.createdAt)
113
+ .where(sql `${columns.status} in ('pending', 'retrying', 'processing')`),
114
+ check(name("deliveries_status_check"), sql `${columns.status} in ('pending', 'processing', 'retrying', 'sent', 'skipped', 'failed', 'canceled')`),
115
+ check(name("deliveries_attempts_check"), sql `${columns.attempts} >= 0 and ${columns.maxAttempts} > 0`),
116
+ ]);
117
+ const preferences = table("preferences", {
118
+ recipientId: text("recipient_id").notNull(),
119
+ topic: varchar("topic", { length: 191 }).notNull(),
120
+ channel: varchar("channel", { length: 64 }).notNull(),
121
+ enabled: boolean("enabled").notNull(),
122
+ createdAt: timestamp("created_at", {
123
+ withTimezone: true,
124
+ mode: "date",
125
+ })
126
+ .notNull()
127
+ .defaultNow(),
128
+ updatedAt: timestamp("updated_at", {
129
+ withTimezone: true,
130
+ mode: "date",
131
+ })
132
+ .notNull()
133
+ .defaultNow(),
134
+ }, (columns) => [
135
+ primaryKey({
136
+ name: name("preferences_pk"),
137
+ columns: [columns.recipientId, columns.topic, columns.channel],
138
+ }),
139
+ index(name("preferences_recipient_idx")).on(columns.recipientId),
140
+ ]);
141
+ return {
142
+ notifications,
143
+ notificationDeliveries: deliveries,
144
+ notificationPreferences: preferences,
145
+ };
146
+ }
147
+ export const notificationSchema = createNotificationSchema();
148
+ export const { notifications, notificationDeliveries, notificationPreferences, } = notificationSchema;
149
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ export type JsonPrimitive = boolean | number | string | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type JsonObject = {
6
+ [key: string]: JsonValue;
7
+ };
8
+ export declare const BUILT_IN_CHANNELS: {
9
+ readonly IN_APP: "in_app";
10
+ readonly EMAIL: "email";
11
+ readonly SMS: "sms";
12
+ readonly PUSH: "push";
13
+ };
14
+ export type BuiltInChannel = (typeof BUILT_IN_CHANNELS)[keyof typeof BUILT_IN_CHANNELS];
15
+ export declare const PREFERENCE_WILDCARD: "*";
16
+ export type DeliveryStatus = "pending" | "processing" | "retrying" | "sent" | "skipped" | "failed" | "canceled";
17
+ export type NotificationPayload<TChannel extends string = string, TData extends JsonObject = JsonObject> = {
18
+ title: string;
19
+ body: string;
20
+ actionUrl?: string;
21
+ data?: TData;
22
+ channelData?: Partial<Record<TChannel, JsonValue>>;
23
+ };
24
+ export type DeliveryResult = {
25
+ providerMessageId?: string;
26
+ metadata?: JsonObject;
27
+ };
28
+ export type DeliveryContext<TChannel extends string = string, TData extends JsonObject = JsonObject> = {
29
+ notificationId: string;
30
+ deliveryId: string;
31
+ recipientId: string;
32
+ topic: string;
33
+ channel: TChannel;
34
+ payload: NotificationPayload<TChannel, TData>;
35
+ channelData: JsonValue | undefined;
36
+ attempt: number;
37
+ maxAttempts: number;
38
+ createdAt: Date;
39
+ scheduledFor: Date;
40
+ actorId: string | null;
41
+ entityType: string | null;
42
+ entityId: string | null;
43
+ };
44
+ export type DeliveryAdapter<TChannel extends string = string, TData extends JsonObject = JsonObject> = (context: DeliveryContext<TChannel, TData>) => Promise<DeliveryResult | void>;
45
+ export type DeliveryAdapters<TChannel extends string, TData extends JsonObject = JsonObject> = Partial<{
46
+ [TKey in TChannel]: DeliveryAdapter<TKey, TData>;
47
+ }>;
48
+ export type PreferenceDefaults<TChannel extends string> = Record<string, Partial<Record<TChannel, boolean>>>;
49
+ export type PreferenceOverride<TChannel extends string = string> = {
50
+ recipientId: string;
51
+ topic: string | typeof PREFERENCE_WILDCARD;
52
+ channel: TChannel | typeof PREFERENCE_WILDCARD;
53
+ enabled: boolean;
54
+ };
55
+ export type ResolvedPreference<TChannel extends string = string> = {
56
+ topic: string;
57
+ channel: TChannel;
58
+ enabled: boolean;
59
+ source: "exact" | "topic" | "channel" | "recipient" | "topic_default" | "global_default";
60
+ };
61
+ export type PublishInput<TChannel extends string, TData extends JsonObject = JsonObject> = {
62
+ recipientIds: readonly string[];
63
+ topic: string;
64
+ payload: NotificationPayload<TChannel, TData>;
65
+ channels?: readonly TChannel[];
66
+ actorId?: string | null;
67
+ entityType?: string | null;
68
+ entityId?: string | null;
69
+ idempotencyKey?: string;
70
+ scheduledFor?: Date;
71
+ preferencePolicy?: "respect" | "ignore";
72
+ maxAttempts?: number | Partial<Record<TChannel, number>>;
73
+ };
74
+ export type PublishedNotification<TChannel extends string = string> = {
75
+ notificationId: string;
76
+ recipientId: string;
77
+ created: boolean;
78
+ deliveries: Array<{
79
+ deliveryId: string;
80
+ channel: TChannel;
81
+ status: DeliveryStatus;
82
+ }>;
83
+ };
84
+ export type DispatchResult<TChannel extends string = string> = {
85
+ deliveryId: string;
86
+ notificationId: string;
87
+ recipientId: string;
88
+ channel: TChannel;
89
+ status: "sent" | "retrying" | "failed";
90
+ attempt: number;
91
+ errorCode?: string;
92
+ errorMessage?: string;
93
+ };
94
+ export type InboxItem<TData extends JsonObject = JsonObject> = {
95
+ id: string;
96
+ recipientId: string;
97
+ topic: string;
98
+ title: string;
99
+ body: string;
100
+ actionUrl: string | null;
101
+ data: TData;
102
+ actorId: string | null;
103
+ entityType: string | null;
104
+ entityId: string | null;
105
+ readAt: Date | null;
106
+ archivedAt: Date | null;
107
+ createdAt: Date;
108
+ };
109
+ export type InboxCursor = {
110
+ createdAt: Date;
111
+ id: string;
112
+ };
113
+ export type InboxPage<TData extends JsonObject = JsonObject> = {
114
+ items: Array<InboxItem<TData>>;
115
+ nextCursor: InboxCursor | null;
116
+ };
117
+ export type NotificationLogger = {
118
+ debug?: (message: string, context?: JsonObject) => void;
119
+ error?: (message: string, context?: JsonObject) => void;
120
+ };
121
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ export const BUILT_IN_CHANNELS = {
2
+ IN_APP: "in_app",
3
+ EMAIL: "email",
4
+ SMS: "sms",
5
+ PUSH: "push",
6
+ };
7
+ export const PREFERENCE_WILDCARD = "*";
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AASA,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;CACJ,CAAC;AAKX,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAY,CAAC"}
@@ -0,0 +1,45 @@
1
+ # Adoption notes
2
+
3
+ The package was shaped against two existing PostgreSQL/Drizzle applications
4
+ with complementary notification capabilities.
5
+
6
+ ## Application with Web Push
7
+
8
+ Keep Push subscription persistence and VAPID delivery in the application.
9
+ Register its existing `sendPushToUsers()` function as the `push` adapter.
10
+ Incoming-call and online-presence features publish topics through UNC,
11
+ which adds preference checks, durable delivery status, idempotency, and an
12
+ optional in-app companion.
13
+
14
+ UNC does not replace Web Push endpoint cleanup. A 404/410 response is
15
+ provider-specific subscription lifecycle behavior and remains in the adapter.
16
+
17
+ ## CRM with email and SMS infrastructure
18
+
19
+ Keep mailbox/domain verification, suppressions, SMTP retry rows, SMS carriers,
20
+ virtual SIM selection, and device OTP handling in the CRM. Register:
21
+
22
+ - `enqueueEmail()` or `sendEmail()` as the `email` adapter;
23
+ - the selected carrier/queue function as the `sms` adapter.
24
+
25
+ If the existing mail function enqueues durably, the UNC delivery becomes
26
+ `sent` when enqueue succeeds. SMTP attempts and bounce events remain in the
27
+ mail subsystem. The returned mail row id can be stored as
28
+ `providerMessageId`.
29
+
30
+ The CRM's existing reminder rows are a domain scheduling feature (entity/date/
31
+ remind-before semantics). They should publish a UNC notification when due, not
32
+ be silently reinterpreted as UNC delivery rows.
33
+
34
+ ## Deliberate non-goals
35
+
36
+ - No provider account/domain management.
37
+ - No contact database.
38
+ - No template editor or renderer.
39
+ - No audience/campaign builder.
40
+ - No HTTP routes or UI.
41
+ - No automatic migration of existing application tables.
42
+ - No in-process singleton queue.
43
+
44
+ Those capabilities belong to consuming applications or focused provider
45
+ packages.
@@ -0,0 +1,75 @@
1
+ # Architecture
2
+
3
+ ## Data ownership
4
+
5
+ UNC owns three tables:
6
+
7
+ - `unc_notifications`: one logical notification per recipient;
8
+ - `unc_deliveries`: one delivery per notification and selected channel;
9
+ - `unc_preferences`: sparse recipient overrides by topic and channel.
10
+
11
+ One notification per recipient is intentional. Read/archive state and
12
+ idempotency are recipient-specific, and it avoids arrays of recipient ids that
13
+ are difficult to authorize or index.
14
+
15
+ ## Delivery lifecycle
16
+
17
+ ```text
18
+ pending -> processing -> sent
19
+ \-> retrying -> processing
20
+ \-> failed
21
+ pending/retrying/processing -> canceled
22
+ selected but opted out -> skipped
23
+ ```
24
+
25
+ Publishing and creating delivery rows occurs in one database transaction.
26
+ Provider callbacks never run inside that transaction.
27
+
28
+ `dispatchDue()` claims a bounded set of due rows in a transaction using
29
+ `FOR UPDATE SKIP LOCKED`, changes them to `processing`, commits, and only then
30
+ invokes adapters. The `lockedBy` token prevents a late worker from overwriting a
31
+ row reclaimed after its lock expired.
32
+
33
+ ## In-app as a channel
34
+
35
+ `in_app` uses the same preference and scheduling path as every external
36
+ channel. Its delivery needs no callback: successfully processing it makes the
37
+ notification visible to inbox queries.
38
+
39
+ This avoids the common bug where creating an email-only notification
40
+ accidentally inserts an in-app alert.
41
+
42
+ ## Preference model
43
+
44
+ Only explicit recipient overrides are stored. Application defaults live in
45
+ code, versioned with application behavior. Wildcards make broad choices easy
46
+ without copying every topic/channel combination into the database.
47
+
48
+ Preference checks occur at publication time. The resolved choice is captured in
49
+ the delivery row as `pending` or `skipped`, so changing a preference later does
50
+ not rewrite historical notifications.
51
+
52
+ ## Provider boundary
53
+
54
+ Adapters receive:
55
+
56
+ - UNC notification and delivery ids;
57
+ - recipient id;
58
+ - topic and canonical payload;
59
+ - channel-specific JSON;
60
+ - attempt/max-attempt counts;
61
+ - actor/entity context.
62
+
63
+ The application resolves current contact information and credentials. That
64
+ keeps personally identifiable addresses out of UNC tables and prevents
65
+ stale addresses from being baked into scheduled notifications.
66
+
67
+ ## Delivery semantics
68
+
69
+ UNC provides at-least-once callback invocation. A process can exit
70
+ after a provider accepts a request but before the delivery row is marked sent.
71
+ Applications should pass `deliveryId` as a provider idempotency key whenever the
72
+ provider supports it.
73
+
74
+ Exactly-once delivery cannot be guaranteed across a database and an external
75
+ provider without provider-side idempotency or a distributed transaction.
@@ -0,0 +1,74 @@
1
+ import {
2
+ BUILT_IN_CHANNELS,
3
+ createNotificationCore,
4
+ type NotificationDeliveryError,
5
+ } from "unified-notification-core";
6
+
7
+ import { db } from "./db.js";
8
+ import { notificationSchema } from "./notification-schema.js";
9
+
10
+ type AppChannel =
11
+ | typeof BUILT_IN_CHANNELS.IN_APP
12
+ | typeof BUILT_IN_CHANNELS.EMAIL
13
+ | typeof BUILT_IN_CHANNELS.SMS
14
+ | typeof BUILT_IN_CHANNELS.PUSH;
15
+
16
+ declare function sendMail(input: {
17
+ userId: string;
18
+ subject: string;
19
+ text: string;
20
+ }): Promise<{ id: string }>;
21
+
22
+ declare function sendSMS(input: {
23
+ userId: string;
24
+ text: string;
25
+ }): Promise<{ id: string }>;
26
+
27
+ declare function sendPush(
28
+ userId: string,
29
+ payload: { title: string; body: string; actionUrl?: string },
30
+ ): Promise<{ sent: number }>;
31
+
32
+ const core = createNotificationCore<AppChannel>({
33
+ db,
34
+ schema: notificationSchema,
35
+ adapters: {
36
+ email: async ({ recipientId, payload }) => {
37
+ const result = await sendMail({
38
+ userId: recipientId,
39
+ subject: payload.title,
40
+ text: payload.body,
41
+ });
42
+ return { providerMessageId: result.id };
43
+ },
44
+ sms: async ({ recipientId, payload }) => {
45
+ const result = await sendSMS({
46
+ userId: recipientId,
47
+ text: payload.body,
48
+ });
49
+ return { providerMessageId: result.id };
50
+ },
51
+ push: async ({ recipientId, payload }) => {
52
+ const result = await sendPush(recipientId, payload);
53
+ return { metadata: { sentDevices: result.sent } };
54
+ },
55
+ },
56
+ defaultChannels: ["in_app", "push"],
57
+ });
58
+
59
+ await core.publishAndDispatch({
60
+ recipientIds: ["user-123"],
61
+ topic: "appointment.reminder",
62
+ idempotencyKey: "appointment-456:reminder-24h",
63
+ payload: {
64
+ title: "Appointment tomorrow",
65
+ body: "Your appointment starts at 10:00.",
66
+ actionUrl: "/appointments/456",
67
+ data: { appointmentId: "456" },
68
+ },
69
+ });
70
+
71
+ // Importing the error as a type above is only to show that adapters may use
72
+ // NotificationDeliveryError for permanent failures.
73
+ type PermanentFailure = NotificationDeliveryError;
74
+ void (null as PermanentFailure | null);
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "unified-notification-core",
3
+ "version": "0.1.0",
4
+ "description": "UNC is a Drizzle-native multichannel notification core with durable delivery, retries, in-app inboxes, and recipient preferences.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./schema": {
14
+ "types": "./dist/schema.d.ts",
15
+ "import": "./dist/schema.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "docs",
21
+ "examples",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "typecheck": "tsc -p tsconfig.json --noEmit",
29
+ "test": "vitest run",
30
+ "check": "npm run typecheck && npm run test && npm run build && npm pack --dry-run",
31
+ "prepare": "npm run build"
32
+ },
33
+ "keywords": [
34
+ "unc",
35
+ "notifications",
36
+ "drizzle",
37
+ "postgresql",
38
+ "email",
39
+ "sms",
40
+ "push",
41
+ "in-app",
42
+ "outbox"
43
+ ],
44
+ "license": "MIT",
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "peerDependencies": {
49
+ "drizzle-orm": ">=0.45.0 <1"
50
+ },
51
+ "devDependencies": {
52
+ "@electric-sql/pglite": "^0.5.4",
53
+ "@types/node": "^26.1.2",
54
+ "drizzle-orm": "^0.45.2",
55
+ "typescript": "^6.0.3",
56
+ "vitest": "^4.1.10"
57
+ }
58
+ }