ProjectR Docs
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)

FunctionDescriptionArgs
dashboardGetAllNotificationsDashboard: 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")…
dashboardGetAllTemplatesDashboard: 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()), }
dashboardGetNotificationDeliveryDashboard: 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() }
dashboardGetNotificationQueueDashboard: Get notification queue (pending) Security: Requires dashboard access{ limit: v.optional(v.number()), includeScheduled: v.optional(v.boolean()), }
dashboardGetNotificationStatsDashboard: 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…
dashboardGetPushConfigDashboard: 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.{}
getNotificationByIdGet 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…
getNotificationPreferencesGet notification preferences for current user Security: Uses authenticated user's ID from session (not from args){}
getPushTokensGet 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){}
getUnreadCountGet unread notification count Used by: _layout.tsx, profile/index.tsx Security: Uses authenticated user's ID from session (not from args){}
getUserNotificationsGet 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")…
getUserNotificationsPaginatedPaginated 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)

FunctionDescriptionArgs
dashboardCancelNotificationsDashboard: 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()), }
dashboardCreateTemplateDashboard: 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…
dashboardDeleteTemplateDashboard: Delete notification template Security: Requires admin access{ templateId: v.id("notificationTemplates"), hardDelete: v.optional(v.boolean()), }
dashboardRetryFailedNotificationsDashboard: Retry failed notifications Security: Requires admin access{ notificationIds: v.optional(v.array(v.id("notifications"))), maxAge: v.optional(v.number()), }
dashboardSendTestNotificationDashboard: 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…
dashboardUpdateTemplateDashboard: 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…
deleteNotificationDelete a notification Used by: use-convex-notifications.ts Security: Verifies authenticated user owns the notification{ notificationId: v.id("notifications"), }
markAllAsReadMark all notifications as read for current user Used by: use-convex-notifications.ts Security: Uses authenticated user's ID (not from args){}
markAsReadMark a notification as read Used by: use-convex-notifications.ts Security: Verifies authenticated user owns the notification{ notificationId: v.id("notifications"), }
processQueueDashboard: Process notification queue Security: Requires admin access{ limit: v.optional(v.number()), }
registerPushTokenRegister 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…
removePushTokenRemove/deactivate a push token Used by: use-convex-notifications.ts (sign-out) Security: Verifies authenticated user owns the token{ expoPushToken: v.string(), }
sendBulkNotificationsDashboard: 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(…
updateNotificationPreferencesUpdate 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)

FunctionDescriptionArgs
getNotificationByIdInternalInternal: Get notification by ID{ notificationId: v.id("notifications"), }
getPushTokensByUserIdInternalGet push tokens for a user (internal - for actions){ userId: v.string(), }
getUnreadCountInternalUnread in-app count, sent as the push badge. */{ userId: v.string() }
listTicketsAwaitingReceiptTickets whose receipt is due: created before before and not yet checked. */{ before: v.number(), limit: v.number() }

internalMutation (7)

FunctionDescriptionArgs
applyPushReceiptsApply 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…
enqueueNotificationSingle 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() }
processPendingToWorkpoolProcess 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()) }
pruneOldPushTicketsDrop ticket rows older than 30 days, a batch at a time. */{}
recordPushOutcomeRecord 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)

FunctionDescriptionArgs
checkPushReceiptsRead 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.{}
sendPushNotificationSend 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"), }

On this page