Skip to content

Messaging & Notifications

SeedTrust has an in-platform inbox for case-related communication, plus three outbound notification channels: push (browser), email (SendGrid), and SMS (Twilio).


Messaging

Structure

Messages are organized into threads (MessageChain), each optionally linked to a case. A thread contains one or more messages and can include multiple participants via a MessageChainGroup.

MessageChain (thread)
├── subject
├── case_id (optional)
├── assigned_admin_id
└── Messages
├── from_id + from_type (sender)
├── to_id + to_type (recipient, NULL for group messages)
├── message (body text)
├── viewed (timestamp, NULL = unread)
└── MessageFiles (attachments)

Access Control

  • Non-admin users (IP, Surrogate, CM, Agency Owner) can only see threads they created or are a recipient of
  • Admins see all threads assigned to them or linked to their cases
  • Messages between non-admins go to the assigned admin inbox (to_admin = True)
  • New threads are automatically assigned to the Escrow Specialist on the case, if one exists

Sending a Message

POST /api/messages # Start a new thread
POST /api/messages/{chain_id} # Reply to existing thread

Both endpoints accept multipart/form-data with a message field and optional files.

File attachments are uploaded to S3 at: messages/{chain_id}/{message_id}/{uuid}.{ext}

Attachment URLs in responses are pre-signed S3 URLs (1-hour expiry).

Unread Tracking

User TypeHow Unread Is Tracked
AdminMessageChain.chain_has_unread_msg_admin = True
Non-AdminMessage.viewed IS NULL

GET /api/messages/unread/count returns the count for the current user.

User Type Mapping

The messaging module uses Flask-style short codes (ip, cm, owner) internally but normalizes them to FastAPI-style names (intended_parent, case_manager, agency_owner) when interfacing with other modules. This mapping is defined in notification/utils.py.

Code: seedtrustapi/src/seedtrust/modules/message/


Push Notifications

How They Work

Push notifications use the Web Push protocol with VAPID keys. The browser subscribes to a push endpoint; the server sends payloads to that endpoint when events occur.

Subscription Lifecycle

1. App fetches VAPID public key
GET /api/notifications/vapid-key
2. ServiceWorker requests browser push permission
3. Browser generates a push subscription (endpoint + keys)
4. App registers subscription with backend
POST /api/notifications/subscribe { enabled: true, endpoint, keys }
5. Backend stores in PushSubscription table
→ Unique per (user_type, user_id, endpoint)
6. When an event occurs → backend sends push to all user's subscriptions
7. On unsubscribe
POST /api/notifications/subscribe { enabled: false }
→ All user subscriptions deleted

Expired or invalid subscriptions (HTTP 404/410 from the push endpoint) are automatically deleted when a send fails.

What Triggers Push Notifications

EventRecipients
Non-admin sends a messageAssigned admin
Admin replies to a messageMessage recipient (non-admin)
Admin sends group messageAll non-admin group members

Additional triggers exist throughout the platform (DR approvals, payment confirmations, etc.) — these are sent via the internal endpoint POST /internal/notification/send using the master API key.

Notification Payload Format

{
"title": "New Message",
"body": "You have a new message.",
"icon": "/icons/icon-192.png",
"data": { "url": "/messages/123" },
"tag": "seedtrust-notification",
"requireInteraction": true,
"vibrate": [200, 100, 200]
}

Code: seedtrustapi/src/seedtrust/modules/notification/


Email Notifications

All emails are sent via SendGrid in production. In local development, Mailpit (local SMTP) is used if MAILPIT_URL is configured — this lets developers see emails without sending them to real addresses.

Sending an Email (FastAPI)

await send_email(
subject="Subject Line",
template_name="message_alert.html",
template_context={"case_name": "Smith Journey"},
run_in_background=True,
background_tasks=background_tasks,
)

Emails can be sent immediately or deferred to a BackgroundTasks queue. For anything that doesn’t need to block the response, use run_in_background=True.

Email templates are Jinja2 files in seedtrustapi/src/seedtrust/modules/emails/templates/.

Common Email Triggers

EventTemplate
New messagemessage_alert.html
DR submitted for approvalApproval request to designated approver
DR approvedConfirmation to surrogate / IP
DR deniedDenial notice to submitter + Escrow Specialist
Pregnancy confirmed (with funding requirement)ip_pregnancy_funding.html to IPs
Last SPC in category paidAdmin notification
Case close reminderEscrow Specialist notification

SMS Notifications

SMS is sent via Twilio for time-sensitive communications (2FA codes, urgent alerts).

await send_sms(phone="+15551234567", message="Your verification code is 123456")

Error handling:

  • Invalid phone format → ValueError (do not retry)
  • Unsubscribed recipient → ValueError (do not retry)
  • Other failures → Exception (may retry)

Gotchas for Developers

Push subscriptions are per-device, not per-user. A user with three devices has three push subscriptions. Sending a notification goes to all of them simultaneously.

Attachment URLs in message responses expire after 1 hour. If a user opens a message thread that was loaded more than an hour ago, attachment links will be broken. The frontend should re-fetch message data to refresh URLs.

The to_admin flag routes messages to the admin inbox. A message with to_admin = True appears in the admin’s unread count even if to_id is NULL. This is how non-admin messages are surfaced to the assigned admin.

Email and push are separate. Sending a push notification does not send an email, and vice versa. Both are triggered independently in the message send flow.


Open Questions

1. Push notification event inventory Which DR, payment, and case events trigger push notifications via /internal/notification/send? A complete event inventory does not currently exist.


Email templates: There is no separate registry — the source of truth for FastAPI email templates is the seedtrustapi/src/seedtrust/modules/emails/templates/ folder. Flask email sending uses SendGrid template IDs defined inline in Flask views.