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 threadPOST /api/messages/{chain_id} # Reply to existing threadBoth 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 Type | How Unread Is Tracked |
|---|---|
| Admin | MessageChain.chain_has_unread_msg_admin = True |
| Non-Admin | Message.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 deletedExpired or invalid subscriptions (HTTP 404/410 from the push endpoint) are automatically deleted when a send fails.
What Triggers Push Notifications
| Event | Recipients |
|---|---|
| Non-admin sends a message | Assigned admin |
| Admin replies to a message | Message recipient (non-admin) |
| Admin sends group message | All 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
| Event | Template |
|---|---|
| New message | message_alert.html |
| DR submitted for approval | Approval request to designated approver |
| DR approved | Confirmation to surrogate / IP |
| DR denied | Denial notice to submitter + Escrow Specialist |
| Pregnancy confirmed (with funding requirement) | ip_pregnancy_funding.html to IPs |
| Last SPC in category paid | Admin notification |
| Case close reminder | Escrow 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.