Backend (Convex)
API — notification
Generated catalog of Convex functions in convex/notification/. Regenerated by `bun run docs:api`.
Auto-generated from source. Do not edit by hand — run
bun run docs:api.
query (12)
| Function | Description | Args |
|---|---|---|
dashboardGetAllNotifications | Dashboard: Get all notifications with filters Security: Requires dashboard access | { limit: v.optional(v.number()), offset: v.optional(v.number()), status: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("delivered")… |
dashboardGetAllTemplates | Dashboard: Get all templates Security: Requires dashboard access | { limit: v.optional(v.number()), offset: v.optional(v.number()), type: v.optional(v.string()), includeInactive: v.optional(v.boolean()), } |
dashboardGetNotificationDelivery | Dashboard: per-device push results for one notification, for the detail sheet. Tokens are shortened; the full value is a credential for that device. | { notificationId: v.string() } |
dashboardGetNotificationQueue | Dashboard: Get notification queue (pending) Security: Requires dashboard access | { limit: v.optional(v.number()), includeScheduled: v.optional(v.boolean()), } |
dashboardGetNotificationStats | Dashboard: notification statistics for a date window. Staff bell events (types starting "admin.") are excluded; they never reach the app. Push is measured per channel: - reach = pushes Expo accepted (sent or delivered) / pushes attempted, where attempted excludes pending, cancelled, suppressed and unknown rows. - no device = users with no registered token; nothing was attempted. - read rate = in-app copies opened / in-app copies delivered. The pre-2026-10-29 keys (byStatus, deliveryRate, ...) stay for older dashboard builds. | { dateFrom: v.optional(v.number()), dateTo: v.optional(v.number()), // Minutes east of UTC for the daily buckets; the dashboard sends its own. tzOffsetMinute… |
dashboardGetPushConfig | Dashboard: whether this deployment can send push at all. Without EXPO_ACCESS_TOKEN every push fails, which the notification pages say once instead of leaving each "Push failed" to look like a device problem. | {} |
getNotificationById | Get a specific notification by ID Used by: use-convex-notifications.ts Security: Verifies the notification belongs to the authenticated user | { // v.string (not v.id) — malformed ids arrive via deep links // (projectr:///(app)/notifications/detail/<id>); a validator throw here // crashes the client… |
getNotificationPreferences | Get notification preferences for current user Security: Uses authenticated user's ID from session (not from args) | {} |
getPushTokens | Get push tokens for current user (device metadata) Used by: use-convex-notifications.ts Note: Actual token storage is handled by the push notifications component Security: Uses authenticated user's ID from session (not from args) | {} |
getUnreadCount | Get unread notification count Used by: _layout.tsx, profile/index.tsx Security: Uses authenticated user's ID from session (not from args) | {} |
getUserNotifications | Get user notifications with pagination Used by: use-convex-notifications.ts, notifications-list-screen.tsx Security: Uses authenticated user's ID from session (not from args) | { limit: v.optional(v.number()), offset: v.optional(v.number()), status: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("delivered")… |
getUserNotificationsPaginated | Paginated sibling of getUserNotifications for Convex usePaginatedQuery (mobile inbox infinite scroll). Same index fan-out and mobile-feed filtering as getUserNotifications, but cursor-driven via .paginate so earlier pages stay reactive instead of re-reading a growing limit window. Note: filtering admin.* rows happens per page AFTER .paginate, so pages can come back short for admin users — isDone/continueCursor stay correct. Security: Uses authenticated user's ID from session (not from args) | { paginationOpts: paginationOptsValidator, status: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("delivered"), v.literal("failed"),… |
mutation (14)
| Function | Description | Args |
|---|---|---|
dashboardCancelNotifications | Dashboard: Cancel pending notifications Security: Requires admin access | { notificationIds: v.optional(v.array(v.id("notifications"))), userId: v.optional(v.string()), type: v.optional(v.string()), } |
dashboardCreateTemplate | Dashboard: Create notification template Security: Requires admin access | { name: v.string(), description: v.optional(v.string()), type: v.string(), channels: v.array(v.union(v.literal("push"), v.literal("in_app"))), pushTitle: v.o… |
dashboardDeleteTemplate | Dashboard: Delete notification template Security: Requires admin access | { templateId: v.id("notificationTemplates"), hardDelete: v.optional(v.boolean()), } |
dashboardRetryFailedNotifications | Dashboard: Retry failed notifications Security: Requires admin access | { notificationIds: v.optional(v.array(v.id("notifications"))), maxAge: v.optional(v.number()), } |
dashboardSendTestNotification | Dashboard: send a notification to yourself, to check the copy and the device before a real send. Returns how many active devices you have, so a test that cannot push says so up front. | { type: v.string(), title: v.string(), body: v.string(), data: v.optional(v.record(v.string(), v.any())), channels: v.array(v.union(v.literal("push"), v.lite… |
dashboardUpdateTemplate | Dashboard: Update notification template Security: Requires admin access | { templateId: v.id("notificationTemplates"), name: v.optional(v.string()), description: v.optional(v.string()), type: v.optional(v.string()), channels: v.opt… |
deleteNotification | Delete a notification Used by: use-convex-notifications.ts Security: Verifies authenticated user owns the notification | { notificationId: v.id("notifications"), } |
markAllAsRead | Mark all notifications as read for current user Used by: use-convex-notifications.ts Security: Uses authenticated user's ID (not from args) | {} |
markAsRead | Mark a notification as read Used by: use-convex-notifications.ts Security: Verifies authenticated user owns the notification | { notificationId: v.id("notifications"), } |
processQueue | Dashboard: Process notification queue Security: Requires admin access | { limit: v.optional(v.number()), } |
registerPushToken | Register a push token for current user Used by: use-notification-initialization.ts (mobile app) Stores token in BOTH places: 1. Expo Push Notifications component - source of truth for token 2. pushTokens table - device metadata (name, type, lastUsedAt) Security: Uses authenticated user's ID (not from args) | { expoPushToken: v.string(), deviceType: v.optional(v.string()), deviceName: v.optional(v.string()), // Mobile app sends userId but it's ignored — derived fr… |
removePushToken | Remove/deactivate a push token Used by: use-convex-notifications.ts (sign-out) Security: Verifies authenticated user owns the token | { expoPushToken: v.string(), } |
sendBulkNotifications | Dashboard: Send bulk notifications Security: Requires dashboard access | { userIds: v.array(v.string()), type: v.string(), title: v.string(), body: v.string(), data: v.optional(v.record(v.string(), v.any())), channels: v.optional(… |
updateNotificationPreferences | Update notification preferences for current user Security: Uses authenticated user's ID (not from args) | { pushEnabled: v.optional(v.boolean()), inAppEnabled: v.optional(v.boolean()), emailEnabled: v.optional(v.boolean()), disabledTypes: v.optional(v.array(v.str… |
internalQuery (4)
| Function | Description | Args |
|---|---|---|
getNotificationByIdInternal | Internal: Get notification by ID | { notificationId: v.id("notifications"), } |
getPushTokensByUserIdInternal | Get push tokens for a user (internal - for actions) | { userId: v.string(), } |
getUnreadCountInternal | Unread in-app count, sent as the push badge. */ | { userId: v.string() } |
listTicketsAwaitingReceipt | Tickets whose receipt is due: created before before and not yet checked. */ | { before: v.number(), limit: v.number() } |
internalMutation (7)
| Function | Description | Args |
|---|---|---|
applyPushReceipts | Apply Expo receipts. A notification becomes delivered once any of its tickets has an ok receipt, and failed once every ticket has errored. | { updates: v.array( v.object({ ticketRowId: v.id("pushTickets"), status: v.union(v.literal("ok"), v.literal("error"), v.literal("unknown")), error: v.optiona… |
broadcastToAdmins | — | { type: v.string(), title: v.string(), body: v.string(), data: v.optional(v.any()), priority: v.optional( v.union( v.literal("low"), v.literal("medium"), v.l… |
enqueueNotification | Single notification pipeline entry point. ALL notification producers (webhooks, gamification, dashboard, crons) should call this mutation instead of creating notifications directly. This ensures: 1. Policy enforcement is transactional (same transaction as insert) 2. Push delivery goes through workpool (parallelism + retry control) 3. Policy result is stored inline for debugging/auditing | { userId: v.string(), type: v.string(), title: v.string(), body: v.string(), data: v.optional(v.record(v.string(), v.any())), channels: v.optional(v.array(v.… |
markAllAsReadContinue | — | { userId: v.string() } |
processPendingToWorkpool | Process pending notifications via workpool (for cron job). A mutation, not an action, so it does not spend one function run per notification. It runs in microseconds and enqueues push delivery through notificationPool (bounded concurrency). | { limit: v.optional(v.number()) } |
pruneOldPushTickets | Drop ticket rows older than 30 days, a batch at a time. */ | {} |
recordPushOutcome | Record what Expo said about one notification's push. Writes one pushTickets row per device, deactivates tokens Expo calls dead, and sets the row and push-channel status: - no device: push no_device; the row stays sent when the in-app copy landed, and is failed for a push-only send. - any device accepted: push sent, row sent. - every device rejected: push failed, row failed, with each distinct reason. | { notificationId: v.id("notifications"), tickets: v.array(ticketValidator), } |
internalAction (2)
| Function | Description | Args |
|---|---|---|
checkPushReceipts | Read Expo receipts for tickets older than 15 minutes (Expo's guidance) and record whether Apple or Google accepted each push. A ticket with no receipt after a day is marked unknown; Expo keeps receipts for 24 hours. | {} |
sendPushNotification | Send one notification's push to every active device of its user. Expo answers each message with a ticket. An ok ticket means Expo queued it; whether Apple or Google took it shows up in the receipt, which checkPushReceipts reads later. A user with no active device is recorded as no_device, not as a delivery failure: nothing was attempted. | { notificationId: v.id("notifications"), } |