Three separate systems live here:
  • WhatsApp, in the public schema.
  • Trainee push notifications, in the public schema.
  • The coach web app’s notification bell, in the auth schema.

WhatsApp

WhatsappTemplate

Table whatsapp_templates. A message template the studio wrote in Perform. These are Perform-side text templates. Approved WhatsApp Business templates live in the studio’s SmartSend account and are listed live through the SmartSend API. They are not stored here.

WhatsappMessage

Table whatsapp_messages. A log of messages sent and received. Indexes: studioId, clientId, (clientId, createdAt), (studioId, createdAt).

MessageStatus

An inbound message from a known trainee also creates an InboxItem of type MESSAGE with refId pointing at the message row.

Trainee push

TraineePushToken

Table trainee_push_tokens. One row per device. Unique on (clientId, token). Lifecycle:
  • Registered by POST /v1/trainee/push-token, removed by DELETE /v1/trainee/push-token.
  • Pruned by prunePushTokens when Expo reports DeviceNotRegistered, on a ticket or on a receipt.
  • Deleted explicitly when a trainee deletes their account, because that is a soft delete and the cascade never fires.

NotificationConfigType

Fifteen values. Twelve are active (ACTIVE_NOTIFICATION_TYPES in notification-configs.schema.ts). The three workout reminder types are scheduled on the device by the app. The server only supplies their text.

NotificationTypeConfig

Table notification_type_configs. The global default for each type. Managed through /v1/admin/notification-configs with service auth.

StudioNotificationConfig

Table studio_notification_configs. A studio’s override of one type. Same columns as the global row plus studioId. Unique on (studioId, type). Index on studioId. resolveNotificationConfig in apps/core-api/src/lib/trainee-push.ts picks the studio row, then the global row, then the code defaults in NOTIFICATION_CONFIG_DEFAULTS.

messages

Most types are single: one title and body, taken from titleTemplate and bodyTemplate. Two types hold several messages in the messages column, validated by notificationMessage:
Limits from NOTIFICATION_CATALOGUE: up to 3 messages for FORM_SENT (MAX_FORM_SENT_MESSAGES) and up to 10 for FORM_REMINDERS (MAX_FORM_REMINDERS). The replace endpoint accepts 1 to 20. When the stored messages value does not parse, messagesOf falls back to the code defaults for that type.

Trainee mute groups

Client.mutedNotificationGroups holds the groups a trainee switched off in the app: CHECK_INS, FORMS, PLANS, WORKOUT_REMINDERS, COACH_MESSAGES, SUBSCRIPTION. isNotificationMuted(type, mutedGroups, formType) maps a type to its group with notificationGroupFor. COACH_MESSAGES is in LOCKED_NOTIFICATION_GROUPS and can never be muted, whatever the array says. A muted recipient is reported in the muted list of the send result. It is not counted as a failure. Sending, tickets and receipts are covered in Push notifications.

Coach web notifications

These live in the auth schema and are declared only in packages/database. The code is in packages/notifications.

Notification

Table auth.notification.

UserNotificationPreference

Table auth.user_notification_preference. A row means the user turned that type off for that target. Unique on (userId, type, target). createNotification in packages/notifications/src/create-notification.ts checks isNotificationDisabled for each target. It inserts the in-app row unless in-app is disabled, and sends the notification email template unless email is disabled. The settings page groups come from NOTIFICATION_GROUPS in packages/notifications/src/catalog.ts. Today there is one group, general, containing APP_UPDATE. The bell in the web app’s navigation bar reads these rows through the oRPC procedures notifications.unreadCount and notifications.list. The queries are in packages/database/prisma/queries/notifications.ts.
Coach tasks on the Work Board are a different thing. They are InboxItem rows in the public schema. See Automation and tasks.