convex-notifications 1.3.0 → 1.5.1

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.
Files changed (118) hide show
  1. package/dist/client/index.d.ts +114 -10
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +239 -41
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/types.d.ts +47 -6
  6. package/dist/client/types.d.ts.map +1 -1
  7. package/dist/component/_generated/api.d.ts +8 -0
  8. package/dist/component/_generated/api.d.ts.map +1 -1
  9. package/dist/component/_generated/api.js.map +1 -1
  10. package/dist/component/_generated/component.d.ts +50 -4
  11. package/dist/component/_generated/component.d.ts.map +1 -1
  12. package/dist/component/channels/index.d.ts +28 -0
  13. package/dist/component/channels/index.d.ts.map +1 -0
  14. package/dist/component/channels/index.js +28 -0
  15. package/dist/component/channels/index.js.map +1 -0
  16. package/dist/component/channels/types.d.ts +66 -0
  17. package/dist/component/channels/types.d.ts.map +1 -0
  18. package/dist/component/channels/types.js +8 -0
  19. package/dist/component/channels/types.js.map +1 -0
  20. package/dist/component/channels/validators.d.ts +53 -0
  21. package/dist/component/channels/validators.d.ts.map +1 -0
  22. package/dist/component/channels/validators.js +101 -0
  23. package/dist/component/channels/validators.js.map +1 -0
  24. package/dist/component/convex.config.d.ts +2 -2
  25. package/dist/component/convex.config.d.ts.map +1 -1
  26. package/dist/component/convex.config.js +8 -1
  27. package/dist/component/convex.config.js.map +1 -1
  28. package/dist/component/crons.d.ts +3 -0
  29. package/dist/component/crons.d.ts.map +1 -0
  30. package/dist/component/crons.js +26 -0
  31. package/dist/component/crons.js.map +1 -0
  32. package/dist/component/delivery.d.ts +15 -0
  33. package/dist/component/delivery.d.ts.map +1 -1
  34. package/dist/component/delivery.js +53 -1
  35. package/dist/component/delivery.js.map +1 -1
  36. package/dist/component/fallback.d.ts +101 -0
  37. package/dist/component/fallback.d.ts.map +1 -0
  38. package/dist/component/fallback.js +189 -0
  39. package/dist/component/fallback.js.map +1 -0
  40. package/dist/component/inbox.d.ts +26 -17
  41. package/dist/component/inbox.d.ts.map +1 -1
  42. package/dist/component/inbox.js +80 -31
  43. package/dist/component/inbox.js.map +1 -1
  44. package/dist/component/notifications.d.ts +47 -0
  45. package/dist/component/notifications.d.ts.map +1 -1
  46. package/dist/component/notifications.js +184 -0
  47. package/dist/component/notifications.js.map +1 -1
  48. package/dist/component/preferences.d.ts +4 -0
  49. package/dist/component/preferences.d.ts.map +1 -1
  50. package/dist/component/preferences.js +33 -14
  51. package/dist/component/preferences.js.map +1 -1
  52. package/dist/component/pushTokens.d.ts +4 -0
  53. package/dist/component/pushTokens.d.ts.map +1 -1
  54. package/dist/component/pushTokens.js +34 -13
  55. package/dist/component/pushTokens.js.map +1 -1
  56. package/dist/component/retry.d.ts +86 -0
  57. package/dist/component/retry.d.ts.map +1 -0
  58. package/dist/component/retry.js +176 -0
  59. package/dist/component/retry.js.map +1 -0
  60. package/dist/component/scheduled.d.ts +101 -0
  61. package/dist/component/scheduled.d.ts.map +1 -0
  62. package/dist/component/scheduled.js +177 -0
  63. package/dist/component/scheduled.js.map +1 -0
  64. package/dist/component/schema.d.ts +117 -4
  65. package/dist/component/schema.d.ts.map +1 -1
  66. package/dist/component/schema.js +77 -4
  67. package/dist/component/schema.js.map +1 -1
  68. package/dist/component/validators.d.ts +160 -0
  69. package/dist/component/validators.d.ts.map +1 -0
  70. package/dist/component/validators.js +95 -0
  71. package/dist/component/validators.js.map +1 -0
  72. package/dist/component/webhooks/index.d.ts +33 -0
  73. package/dist/component/webhooks/index.d.ts.map +1 -0
  74. package/dist/component/webhooks/index.js +33 -0
  75. package/dist/component/webhooks/index.js.map +1 -0
  76. package/dist/component/webhooks/resend.d.ts +48 -0
  77. package/dist/component/webhooks/resend.d.ts.map +1 -0
  78. package/dist/component/webhooks/resend.js +164 -0
  79. package/dist/component/webhooks/resend.js.map +1 -0
  80. package/dist/component/webhooks/twilio.d.ts +48 -0
  81. package/dist/component/webhooks/twilio.d.ts.map +1 -0
  82. package/dist/component/webhooks/twilio.js +154 -0
  83. package/dist/component/webhooks/twilio.js.map +1 -0
  84. package/dist/react/index.d.ts +148 -4
  85. package/dist/react/index.d.ts.map +1 -1
  86. package/dist/react/index.js +154 -3
  87. package/dist/react/index.js.map +1 -1
  88. package/package.json +16 -2
  89. package/src/client/index.test.ts +14 -14
  90. package/src/client/index.ts +308 -52
  91. package/src/client/types.ts +46 -6
  92. package/src/component/_generated/api.ts +8 -0
  93. package/src/component/_generated/component.ts +61 -9
  94. package/src/component/channels/channels.test.ts +158 -0
  95. package/src/component/channels/index.ts +46 -0
  96. package/src/component/channels/types.ts +70 -0
  97. package/src/component/channels/validators.ts +119 -0
  98. package/src/component/convex.config.ts +10 -1
  99. package/src/component/crons.ts +51 -0
  100. package/src/component/delivery.test.ts +316 -0
  101. package/src/component/delivery.ts +61 -1
  102. package/src/component/fallback.test.ts +315 -0
  103. package/src/component/fallback.ts +218 -0
  104. package/src/component/inbox.test.ts +13 -14
  105. package/src/component/inbox.ts +96 -37
  106. package/src/component/notifications.ts +211 -0
  107. package/src/component/preferences.ts +41 -16
  108. package/src/component/pushTokens.ts +40 -13
  109. package/src/component/retry.test.ts +340 -0
  110. package/src/component/retry.ts +210 -0
  111. package/src/component/scheduled.test.ts +250 -0
  112. package/src/component/scheduled.ts +203 -0
  113. package/src/component/schema.ts +94 -4
  114. package/src/component/validators.ts +132 -0
  115. package/src/component/webhooks/index.ts +33 -0
  116. package/src/component/webhooks/resend.ts +237 -0
  117. package/src/component/webhooks/twilio.ts +201 -0
  118. package/src/react/index.ts +219 -7
@@ -3,8 +3,34 @@ import type { NotificationsOptions, NotificationDefinition, RunQueryCtx, RunMuta
3
3
  import type { PushNotifications } from "@convex-dev/expo-push-notifications";
4
4
  import type { Resend } from "@convex-dev/resend";
5
5
  import type { Twilio } from "@convex-dev/twilio";
6
- export type { NotificationsOptions, NotificationDefinition } from "./types.js";
6
+ export type { NotificationsOptions, NotificationDefinition, AuthIdentity } from "./types.js";
7
7
  export type { ChannelTemplates, EmailTemplate, InboxTemplate, PushTemplate, SmsTemplate, ChannelConfig, EmailChannelConfig, PushChannelConfig, SmsChannelConfig, DeliveryResult, SendResult, RunQueryCtx, RunMutationCtx, RunActionCtx, } from "./types.js";
8
+ /**
9
+ * Create a typed notification definition.
10
+ *
11
+ * This helper function provides runtime validation and type inference
12
+ * for notification definitions.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const welcomeNotification = createNotification({
17
+ * event: "user.welcome",
18
+ * dataValidator: v.object({ userName: v.string() }),
19
+ * category: "onboarding",
20
+ * channels: {
21
+ * inbox: {
22
+ * title: (data) => `Welcome, ${data.userName}!`,
23
+ * body: () => "Thanks for joining.",
24
+ * },
25
+ * email: {
26
+ * subject: (data) => `Welcome, ${data.userName}!`,
27
+ * body: (data) => `Hi ${data.userName}, welcome aboard!`,
28
+ * },
29
+ * },
30
+ * });
31
+ * ```
32
+ */
33
+ export declare function createNotification<T>(definition: NotificationDefinition<T>): NotificationDefinition<T>;
8
34
  /**
9
35
  * Extended options that include child component clients for delivery.
10
36
  */
@@ -37,12 +63,21 @@ export declare class Notifications {
37
63
  component: ComponentApi;
38
64
  options: NotificationsWithChannelsOptions;
39
65
  constructor(component: ComponentApi, options: NotificationsWithChannelsOptions);
40
- list(ctx: RunQueryCtx, opts?: {
41
- limit?: number;
42
- cursor?: number;
66
+ /**
67
+ * Resolve the auth identity and return userId + optional tenantId.
68
+ */
69
+ private resolveAuth;
70
+ /**
71
+ * Resolve channel config, which may be static or a per-tenant function.
72
+ */
73
+ private resolveChannelConfig;
74
+ list(ctx: RunQueryCtx, paginationOpts: {
75
+ numItems: number;
76
+ cursor: string | null;
43
77
  }): Promise<{
44
- cursor: number | null;
45
- notifications: any[];
78
+ page: any[];
79
+ isDone: boolean;
80
+ continueCursor: string;
46
81
  }>;
47
82
  unreadCount(ctx: RunQueryCtx): Promise<number>;
48
83
  markRead(ctx: RunMutationCtx, notificationId: string): Promise<null>;
@@ -70,6 +105,7 @@ export declare class Notifications {
70
105
  getPushTokens(ctx: RunQueryCtx): Promise<{
71
106
  _id: string;
72
107
  _creationTime: number;
108
+ tenantId?: string;
73
109
  userId: string;
74
110
  token: string;
75
111
  platform?: "ios" | "android" | "web";
@@ -79,6 +115,33 @@ export declare class Notifications {
79
115
  * Delete a push token.
80
116
  */
81
117
  deletePushToken(ctx: RunMutationCtx, token: string): Promise<boolean>;
118
+ /**
119
+ * Schedule a notification for future delivery.
120
+ *
121
+ * @returns The scheduled notification ID
122
+ */
123
+ schedule<T>(ctx: RunMutationCtx, definition: NotificationDefinition<T>, args: {
124
+ userId: string;
125
+ tenantId?: string;
126
+ data: T;
127
+ scheduledFor: number | Date;
128
+ transactional?: boolean;
129
+ deduplicationKey?: string;
130
+ }): Promise<{
131
+ scheduledNotificationId: string;
132
+ }>;
133
+ /**
134
+ * Cancel a scheduled notification.
135
+ *
136
+ * @returns true if cancelled, false if not found or already processed
137
+ */
138
+ cancelScheduled(ctx: RunMutationCtx, scheduledNotificationId: string): Promise<boolean>;
139
+ /**
140
+ * Get scheduled notifications for the current user.
141
+ */
142
+ getScheduledNotifications(ctx: RunQueryCtx, opts?: {
143
+ status?: "pending" | "processing" | "sent" | "failed" | "cancelled";
144
+ }): Promise<any[]>;
82
145
  /**
83
146
  * Send a notification through all enabled channels.
84
147
  *
@@ -86,6 +149,7 @@ export declare class Notifications {
86
149
  */
87
150
  send<T>(ctx: RunMutationCtx | RunActionCtx, definition: NotificationDefinition<T>, args: {
88
151
  userId: string;
152
+ tenantId?: string;
89
153
  data: T;
90
154
  transactional?: boolean;
91
155
  deduplicationKey?: string;
@@ -121,12 +185,20 @@ export declare class Notifications {
121
185
  /**
122
186
  * List notifications for the current user (paginated)
123
187
  */
188
+ /**
189
+ * List notifications for the current user (paginated).
190
+ * Uses standard Convex pagination via convex-helpers paginator.
191
+ * Compatible with usePaginatedQuery on the client.
192
+ */
124
193
  list: import("convex/server").RegisteredQuery<"public", {
125
- limit?: number;
126
- cursor?: number;
194
+ paginationOpts: {
195
+ numItems: number;
196
+ cursor: string | null;
197
+ };
127
198
  }, Promise<{
128
- cursor: number | null;
129
- notifications: any[];
199
+ page: any[];
200
+ isDone: boolean;
201
+ continueCursor: string;
130
202
  }>>;
131
203
  /**
132
204
  * Get unread notification count for the current user
@@ -161,6 +233,38 @@ export declare class Notifications {
161
233
  channel: string;
162
234
  enabled: boolean;
163
235
  }, Promise<string>>;
236
+ /**
237
+ * Register a push notification token for the current user
238
+ */
239
+ registerPushToken: import("convex/server").RegisteredMutation<"public", {
240
+ token: string;
241
+ platform?: "ios" | "android" | "web";
242
+ deviceId?: string;
243
+ }, Promise<string>>;
244
+ /**
245
+ * Get all push tokens for the current user
246
+ */
247
+ getPushTokens: import("convex/server").RegisteredQuery<"public", {}, Promise<{
248
+ _id: string;
249
+ _creationTime: number;
250
+ tenantId?: string;
251
+ userId: string;
252
+ token: string;
253
+ platform?: "ios" | "android" | "web";
254
+ deviceId?: string;
255
+ }[]>>;
256
+ /**
257
+ * Delete a push token for the current user
258
+ */
259
+ deletePushToken: import("convex/server").RegisteredMutation<"public", {
260
+ token: string;
261
+ }, Promise<boolean>>;
262
+ /**
263
+ * Get delivery logs for a notification
264
+ */
265
+ getDeliveryLogs: import("convex/server").RegisteredQuery<"public", {
266
+ notificationId: string;
267
+ }, Promise<any[]>>;
164
268
  };
165
269
  }
166
270
  export default Notifications;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,55 @@
1
- import { queryGeneric, mutationGeneric } from "convex/server";
1
+ import { queryGeneric, mutationGeneric, paginationOptsValidator } from "convex/server";
2
2
  import { v } from "convex/values";
3
3
  import { dispatchEmail, dispatchPush, dispatchSms, isActionContext, } from "./adapters.js";
4
+ /**
5
+ * Create a typed notification definition.
6
+ *
7
+ * This helper function provides runtime validation and type inference
8
+ * for notification definitions.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const welcomeNotification = createNotification({
13
+ * event: "user.welcome",
14
+ * dataValidator: v.object({ userName: v.string() }),
15
+ * category: "onboarding",
16
+ * channels: {
17
+ * inbox: {
18
+ * title: (data) => `Welcome, ${data.userName}!`,
19
+ * body: () => "Thanks for joining.",
20
+ * },
21
+ * email: {
22
+ * subject: (data) => `Welcome, ${data.userName}!`,
23
+ * body: (data) => `Hi ${data.userName}, welcome aboard!`,
24
+ * },
25
+ * },
26
+ * });
27
+ * ```
28
+ */
29
+ export function createNotification(definition) {
30
+ // Validate required fields
31
+ if (!definition.event || definition.event.trim() === "") {
32
+ throw new Error("Notification definition must have a non-empty 'event' name");
33
+ }
34
+ if (!definition.channels || Object.keys(definition.channels).length === 0) {
35
+ throw new Error("Notification definition must have at least one channel template");
36
+ }
37
+ // Validate that at least inbox is defined (required for all notifications)
38
+ if (!definition.channels.inbox) {
39
+ throw new Error("Notification definition must include an 'inbox' channel template");
40
+ }
41
+ return definition;
42
+ }
43
+ /**
44
+ * Parse an AuthIdentity into userId and optional tenantId.
45
+ * Supports both string (backwards compat) and { userId, tenantId } forms.
46
+ */
47
+ function parseIdentity(identity) {
48
+ if (typeof identity === "string") {
49
+ return { userId: identity };
50
+ }
51
+ return { userId: identity.userId, tenantId: identity.tenantId };
52
+ }
4
53
  export class Notifications {
5
54
  component;
6
55
  options;
@@ -8,52 +57,79 @@ export class Notifications {
8
57
  this.component = component;
9
58
  this.options = options;
10
59
  }
11
- async list(ctx, opts) {
12
- const userId = await this.options.auth(ctx);
60
+ /**
61
+ * Resolve the auth identity and return userId + optional tenantId.
62
+ */
63
+ async resolveAuth(ctx) {
64
+ const identity = await this.options.auth(ctx);
65
+ return parseIdentity(identity);
66
+ }
67
+ /**
68
+ * Resolve channel config, which may be static or a per-tenant function.
69
+ */
70
+ async resolveChannelConfig(tenantId) {
71
+ const channels = this.options.channels;
72
+ if (!channels)
73
+ return undefined;
74
+ if (typeof channels === "function") {
75
+ if (!tenantId) {
76
+ throw new Error("Channel config is a function but no tenantId provided. Use multi-tenant auth or pass tenantId explicitly.");
77
+ }
78
+ return await channels(tenantId);
79
+ }
80
+ return channels;
81
+ }
82
+ async list(ctx, paginationOpts) {
83
+ const { userId, tenantId } = await this.resolveAuth(ctx);
13
84
  return await ctx.runQuery(this.component.inbox.list, {
85
+ tenantId,
14
86
  userId,
15
- ...opts,
87
+ paginationOpts,
16
88
  });
17
89
  }
18
90
  async unreadCount(ctx) {
19
- const userId = await this.options.auth(ctx);
20
- return await ctx.runQuery(this.component.inbox.unreadCount, { userId });
91
+ const { userId, tenantId } = await this.resolveAuth(ctx);
92
+ return await ctx.runQuery(this.component.inbox.unreadCount, { tenantId, userId });
21
93
  }
22
94
  async markRead(ctx, notificationId) {
23
- const userId = await this.options.auth(ctx);
95
+ const { userId, tenantId } = await this.resolveAuth(ctx);
24
96
  return await ctx.runMutation(this.component.inbox.markRead, {
97
+ tenantId,
25
98
  userId,
26
99
  notificationId,
27
100
  });
28
101
  }
29
102
  async markAllRead(ctx) {
30
- const userId = await this.options.auth(ctx);
103
+ const { userId, tenantId } = await this.resolveAuth(ctx);
31
104
  return await ctx.runMutation(this.component.inbox.markAllRead, {
105
+ tenantId,
32
106
  userId,
33
107
  });
34
108
  }
35
109
  async archive(ctx, notificationId) {
36
- const userId = await this.options.auth(ctx);
110
+ const { userId, tenantId } = await this.resolveAuth(ctx);
37
111
  return await ctx.runMutation(this.component.inbox.archive, {
112
+ tenantId,
38
113
  userId,
39
114
  notificationId,
40
115
  });
41
116
  }
42
117
  async getPreferences(ctx) {
43
- const userId = await this.options.auth(ctx);
44
- return await ctx.runQuery(this.component.preferences.getPreferences, { userId });
118
+ const { userId, tenantId } = await this.resolveAuth(ctx);
119
+ return await ctx.runQuery(this.component.preferences.getPreferences, { tenantId, userId });
45
120
  }
46
121
  async updatePreference(ctx, args) {
47
- const userId = await this.options.auth(ctx);
48
- return await ctx.runMutation(this.component.preferences.updatePreference, { userId, ...args });
122
+ const { userId, tenantId } = await this.resolveAuth(ctx);
123
+ return await ctx.runMutation(this.component.preferences.updatePreference, { tenantId, userId, ...args });
49
124
  }
50
125
  /**
51
126
  * Register a push notification token for a user.
52
127
  * Uses the component's internal push token storage.
53
128
  */
54
129
  async registerPushToken(ctx, args) {
55
- const userId = await this.options.auth(ctx);
130
+ const { userId, tenantId } = await this.resolveAuth(ctx);
56
131
  return await ctx.runMutation(this.component.pushTokens.registerPushToken, {
132
+ tenantId,
57
133
  userId,
58
134
  token: args.token,
59
135
  platform: args.platform,
@@ -64,8 +140,9 @@ export class Notifications {
64
140
  * Get all push tokens for the current user.
65
141
  */
66
142
  async getPushTokens(ctx) {
67
- const userId = await this.options.auth(ctx);
143
+ const { userId, tenantId } = await this.resolveAuth(ctx);
68
144
  return await ctx.runQuery(this.component.pushTokens.getPushTokens, {
145
+ tenantId,
69
146
  userId,
70
147
  });
71
148
  }
@@ -73,12 +150,78 @@ export class Notifications {
73
150
  * Delete a push token.
74
151
  */
75
152
  async deletePushToken(ctx, token) {
76
- const userId = await this.options.auth(ctx);
153
+ const { userId, tenantId } = await this.resolveAuth(ctx);
77
154
  return await ctx.runMutation(this.component.pushTokens.deletePushToken, {
155
+ tenantId,
78
156
  userId,
79
157
  token,
80
158
  });
81
159
  }
160
+ /**
161
+ * Schedule a notification for future delivery.
162
+ *
163
+ * @returns The scheduled notification ID
164
+ */
165
+ async schedule(ctx, definition, args) {
166
+ const data = args.data;
167
+ const scheduledFor = args.scheduledFor instanceof Date
168
+ ? args.scheduledFor.getTime()
169
+ : args.scheduledFor;
170
+ // Validate scheduledFor is in the future
171
+ if (scheduledFor <= Date.now()) {
172
+ throw new Error("scheduledFor must be in the future");
173
+ }
174
+ // Render inbox template for storage
175
+ const inboxTemplate = definition.channels.inbox;
176
+ const title = inboxTemplate
177
+ ? inboxTemplate.title(data)
178
+ : definition.event;
179
+ const body = inboxTemplate ? inboxTemplate.body(data) : "";
180
+ // Scope deduplication key to tenant (consistent with send())
181
+ const scopedDeduplicationKey = args.deduplicationKey && args.tenantId
182
+ ? `${args.tenantId}:${args.deduplicationKey}`
183
+ : args.deduplicationKey;
184
+ const scheduledNotificationId = await ctx.runMutation(this.component.scheduled.scheduleNotification, {
185
+ tenantId: args.tenantId,
186
+ userId: args.userId,
187
+ event: definition.event,
188
+ category: definition.category,
189
+ title,
190
+ body,
191
+ data: args.data,
192
+ channels: definition.channels,
193
+ scheduledFor,
194
+ transactional: args.transactional,
195
+ deduplicationKey: scopedDeduplicationKey,
196
+ });
197
+ return { scheduledNotificationId };
198
+ }
199
+ /**
200
+ * Cancel a scheduled notification.
201
+ *
202
+ * @returns true if cancelled, false if not found or already processed
203
+ */
204
+ async cancelScheduled(ctx, scheduledNotificationId) {
205
+ const { userId, tenantId } = await this.resolveAuth(ctx);
206
+ return await ctx.runMutation(this.component.scheduled.cancelScheduledNotification, {
207
+ tenantId,
208
+ // scheduledNotificationId is a string returned by the component;
209
+ // Convex component IDs are serialized as strings across the boundary.
210
+ id: scheduledNotificationId,
211
+ userId,
212
+ });
213
+ }
214
+ /**
215
+ * Get scheduled notifications for the current user.
216
+ */
217
+ async getScheduledNotifications(ctx, opts) {
218
+ const { userId, tenantId } = await this.resolveAuth(ctx);
219
+ return await ctx.runQuery(this.component.scheduled.getScheduledNotifications, {
220
+ tenantId,
221
+ userId,
222
+ status: opts?.status,
223
+ });
224
+ }
82
225
  /**
83
226
  * Send a notification through all enabled channels.
84
227
  *
@@ -86,10 +229,14 @@ export class Notifications {
86
229
  */
87
230
  async send(ctx, definition, args) {
88
231
  const data = args.data;
232
+ const tenantId = args.tenantId;
89
233
  const deliveries = [];
90
- // 1. Check deduplication
234
+ // 1. Check deduplication atomically (scope key to tenant if provided)
91
235
  if (args.deduplicationKey) {
92
- const isDuplicate = await ctx.runQuery(this.component.notifications.checkDeduplication, { key: args.deduplicationKey });
236
+ const scopedKey = tenantId
237
+ ? `${tenantId}:${args.deduplicationKey}`
238
+ : args.deduplicationKey;
239
+ const isDuplicate = await ctx.runMutation(this.component.notifications.checkAndRecordDeduplication, { key: scopedKey, ttlSeconds: args.deduplicationTtlSeconds ?? 86400 });
93
240
  if (isDuplicate) {
94
241
  throw new Error("Duplicate notification suppressed by deduplication key");
95
242
  }
@@ -101,6 +248,7 @@ export class Notifications {
101
248
  : definition.event;
102
249
  const body = inboxTemplate ? inboxTemplate.body(data) : "";
103
250
  const notificationId = await ctx.runMutation(this.component.notifications.createNotification, {
251
+ tenantId,
104
252
  userId: args.userId,
105
253
  event: definition.event,
106
254
  title,
@@ -108,13 +256,7 @@ export class Notifications {
108
256
  data: args.data,
109
257
  transactional: args.transactional,
110
258
  });
111
- // 3. Record deduplication key
112
- if (args.deduplicationKey) {
113
- await ctx.runMutation(this.component.notifications.recordDeduplication, {
114
- key: args.deduplicationKey,
115
- ttlSeconds: args.deduplicationTtlSeconds ?? 86400,
116
- });
117
- }
259
+ // 3. Deduplication key already recorded atomically in step 1
118
260
  // 4. Resolve enabled channels
119
261
  const definedChannels = Object.keys(definition.channels);
120
262
  let enabledChannels;
@@ -123,6 +265,7 @@ export class Notifications {
123
265
  }
124
266
  else {
125
267
  enabledChannels = await ctx.runQuery(this.component.preferences.resolvePreferences, {
268
+ tenantId,
126
269
  userId: args.userId,
127
270
  event: definition.event,
128
271
  category: definition.category,
@@ -133,7 +276,7 @@ export class Notifications {
133
276
  for (const channel of enabledChannels) {
134
277
  if (channel === "inbox")
135
278
  continue;
136
- const deliveryResult = await this.dispatchChannel(ctx, channel, definition, args.userId, data, notificationId);
279
+ const deliveryResult = await this.dispatchChannel(ctx, channel, definition, args.userId, data, notificationId, tenantId);
137
280
  if (deliveryResult) {
138
281
  deliveries.push(deliveryResult);
139
282
  }
@@ -143,20 +286,22 @@ export class Notifications {
143
286
  /**
144
287
  * Dispatch a notification to a specific channel.
145
288
  */
146
- async dispatchChannel(ctx, channel, definition, userId, data, notificationId) {
289
+ async dispatchChannel(ctx, channel, definition, userId, data, notificationId, tenantId) {
147
290
  let rendered;
148
291
  let result;
149
292
  try {
293
+ // Resolve channel config (may be per-tenant)
294
+ const channelConfig = await this.resolveChannelConfig(tenantId);
150
295
  if (channel === "email" && definition.channels.email) {
151
296
  const emailTemplate = definition.channels.email;
152
- const emailConfig = this.options.channels?.email;
297
+ const emailConfig = channelConfig?.email;
153
298
  const resendClient = this.options.clients?.email;
154
299
  // Resolve email address
155
300
  const emailResolver = this.options.resolvers?.email;
156
301
  if (!emailResolver) {
157
302
  throw new Error("Email resolver not configured");
158
303
  }
159
- const toEmail = await emailResolver(ctx, userId);
304
+ const toEmail = await emailResolver(ctx, userId, tenantId);
160
305
  if (!toEmail) {
161
306
  return {
162
307
  channel: "email",
@@ -164,10 +309,15 @@ export class Notifications {
164
309
  error: "No email address for user",
165
310
  };
166
311
  }
312
+ // Resolve "from" address: sender resolver > template override > channel config default
313
+ let fromEmail = emailTemplate.from ?? emailConfig?.defaultFrom ?? "";
314
+ if (tenantId && this.options.senderResolvers?.email) {
315
+ fromEmail = await this.options.senderResolvers.email(ctx, tenantId);
316
+ }
167
317
  // Support async html rendering (e.g., React Email)
168
318
  const html = emailTemplate.html ? await emailTemplate.html(data) : undefined;
169
319
  const renderedEmail = {
170
- from: emailTemplate.from ?? emailConfig?.defaultFrom ?? "",
320
+ from: fromEmail,
171
321
  to: toEmail,
172
322
  subject: emailTemplate.subject(data),
173
323
  body: emailTemplate.body(data),
@@ -175,7 +325,7 @@ export class Notifications {
175
325
  };
176
326
  rendered = renderedEmail;
177
327
  if (!renderedEmail.from) {
178
- throw new Error("No 'from' address configured. Set channels.email.defaultFrom or specify 'from' in the email template.");
328
+ throw new Error("No 'from' address configured. Set channels.email.defaultFrom, configure a senderResolver, or specify 'from' in the email template.");
179
329
  }
180
330
  // Dispatch via Resend if client is configured
181
331
  if (resendClient) {
@@ -193,7 +343,7 @@ export class Notifications {
193
343
  }
194
344
  else if (channel === "push" && definition.channels.push) {
195
345
  const pushTemplate = definition.channels.push;
196
- const pushConfig = this.options.channels?.push;
346
+ const pushConfig = channelConfig?.push;
197
347
  const pushClient = this.options.clients?.push;
198
348
  const renderedPush = {
199
349
  userId,
@@ -217,14 +367,14 @@ export class Notifications {
217
367
  }
218
368
  else if (channel === "sms" && definition.channels.sms) {
219
369
  const smsTemplate = definition.channels.sms;
220
- const smsConfig = this.options.channels?.sms;
370
+ const smsConfig = channelConfig?.sms;
221
371
  const twilioClient = this.options.clients?.sms;
222
372
  // Resolve phone number
223
373
  const phoneResolver = this.options.resolvers?.phone;
224
374
  if (!phoneResolver) {
225
375
  throw new Error("Phone resolver not configured");
226
376
  }
227
- const toPhone = await phoneResolver(ctx, userId);
377
+ const toPhone = await phoneResolver(ctx, userId, tenantId);
228
378
  if (!toPhone) {
229
379
  return {
230
380
  channel: "sms",
@@ -232,14 +382,19 @@ export class Notifications {
232
382
  error: "No phone number for user",
233
383
  };
234
384
  }
385
+ // Resolve "from" number: sender resolver > template override > channel config default
386
+ let fromPhone = smsTemplate.from ?? smsConfig?.defaultFrom ?? "";
387
+ if (tenantId && this.options.senderResolvers?.sms) {
388
+ fromPhone = await this.options.senderResolvers.sms(ctx, tenantId);
389
+ }
235
390
  const renderedSms = {
236
- from: smsTemplate.from ?? smsConfig?.defaultFrom ?? "",
391
+ from: fromPhone,
237
392
  to: toPhone,
238
393
  body: smsTemplate.body(data),
239
394
  };
240
395
  rendered = renderedSms;
241
396
  if (!renderedSms.from) {
242
- throw new Error("No 'from' phone number configured. Set channels.sms.defaultFrom or specify 'from' in the SMS template.");
397
+ throw new Error("No 'from' phone number configured. Set channels.sms.defaultFrom, configure a senderResolver, or specify 'from' in the SMS template.");
243
398
  }
244
399
  // Dispatch via Twilio if client is configured AND we have action context
245
400
  if (twilioClient) {
@@ -290,6 +445,7 @@ export class Notifications {
290
445
  ? "pending"
291
446
  : "failed";
292
447
  await ctx.runMutation(this.component.delivery.createDeliveryLog, {
448
+ tenantId,
293
449
  notificationId,
294
450
  channel,
295
451
  status,
@@ -307,6 +463,7 @@ export class Notifications {
307
463
  console.error(`[notifications] ${channel} dispatch failed:`, error);
308
464
  // Create failed delivery log entry
309
465
  await ctx.runMutation(this.component.delivery.createDeliveryLog, {
466
+ tenantId,
310
467
  notificationId,
311
468
  channel,
312
469
  status: "failed",
@@ -357,16 +514,21 @@ export class Notifications {
357
514
  /**
358
515
  * List notifications for the current user (paginated)
359
516
  */
517
+ /**
518
+ * List notifications for the current user (paginated).
519
+ * Uses standard Convex pagination via convex-helpers paginator.
520
+ * Compatible with usePaginatedQuery on the client.
521
+ */
360
522
  list: queryGeneric({
361
523
  args: {
362
- limit: v.optional(v.number()),
363
- cursor: v.optional(v.number()),
524
+ paginationOpts: paginationOptsValidator,
364
525
  },
365
526
  returns: v.object({
366
- notifications: v.array(v.any()),
367
- cursor: v.union(v.number(), v.null()),
527
+ page: v.array(v.any()),
528
+ isDone: v.boolean(),
529
+ continueCursor: v.string(),
368
530
  }),
369
- handler: (ctx, args) => self.list(ctx, args),
531
+ handler: (ctx, args) => self.list(ctx, args.paginationOpts),
370
532
  }),
371
533
  /**
372
534
  * Get unread notification count for the current user
@@ -421,6 +583,42 @@ export class Notifications {
421
583
  returns: v.string(),
422
584
  handler: (ctx, args) => self.updatePreference(ctx, args),
423
585
  }),
586
+ /**
587
+ * Register a push notification token for the current user
588
+ */
589
+ registerPushToken: mutationGeneric({
590
+ args: {
591
+ token: v.string(),
592
+ platform: v.optional(v.union(v.literal("ios"), v.literal("android"), v.literal("web"))),
593
+ deviceId: v.optional(v.string()),
594
+ },
595
+ returns: v.string(),
596
+ handler: (ctx, args) => self.registerPushToken(ctx, args),
597
+ }),
598
+ /**
599
+ * Get all push tokens for the current user
600
+ */
601
+ getPushTokens: queryGeneric({
602
+ args: {},
603
+ returns: v.array(v.any()),
604
+ handler: (ctx) => self.getPushTokens(ctx),
605
+ }),
606
+ /**
607
+ * Delete a push token for the current user
608
+ */
609
+ deletePushToken: mutationGeneric({
610
+ args: { token: v.string() },
611
+ returns: v.boolean(),
612
+ handler: (ctx, args) => self.deletePushToken(ctx, args.token),
613
+ }),
614
+ /**
615
+ * Get delivery logs for a notification
616
+ */
617
+ getDeliveryLogs: queryGeneric({
618
+ args: { notificationId: v.string() },
619
+ returns: v.array(v.any()),
620
+ handler: (ctx, args) => self.getDeliveryLogs(ctx, args.notificationId),
621
+ }),
424
622
  };
425
623
  }
426
624
  }