Overview
Org-wide messages
Timeline posts visible to members/followers based on visibility. Supports @event mentions, likes, and replies.
Event announcements
Targeted messages to event attendees only. Delivered in-app and/or by email, with optional PDF attachments.
- Backend:
Meridian/backend/routes/orgMessageRoutes.js(mounted at/org-messages),Meridian/backend/services/eventAnnouncementService.js,Meridian/backend/services/resendClient.js - Schema:
Meridian/backend/schemas/orgMessage.js - Frontend: Club dashboard message feed, Event Dashboard Communications tab and announcement compose UI
Data model
Data model
Org messaging is stored as OrgMessage documents (collection:
Event announcements are excluded from the org timeline; they appear only in the event’s Communications tab.
orgMessages).Core fields
| Field | Type | Description |
|---|---|---|
orgId | ObjectId | Organization (ref: Org) |
authorId | ObjectId | Author (ref: User) |
content | String | Message body; max length from org/system config (hard cap 2000) |
visibility | String | members_only | members_and_followers | public |
mentionedEvents | ObjectId[] | Events referenced in content (from @event parsing) |
links | String[] | Parsed links from content |
likes | ObjectId[] | User IDs who liked |
likeCount | Number | Denormalized like count |
parentMessageId | ObjectId | Set for replies (ref: OrgMessage) |
replyCount | Number | Denormalized reply count |
isDeleted | Boolean | Soft delete flag |
deletedAt | Date | When soft-deleted |
Event-announcement fields
When the message is an event-specific announcement (eventId set), these apply:| Field | Type | Description |
|---|---|---|
eventId | ObjectId | Event (ref: Event); only org-hosted events |
subject | String | Optional email subject (max 200 chars) |
sendAsOrg | Boolean | If true, display org name/avatar instead of author |
Configuration
Configuration
Per-org: Org.messageSettings
- Effective character limit =
Math.min(org.messageSettings.characterLimit, systemConfig.messaging.maxCharacterLimit).
Global: OrgManagementConfig.messaging
maxCharacterLimit— hard ceilingminCharacterLimit— hard floorrequireProfanityFilter— run content through profanity filter before save/sendnotificationSettings.notifyOnEventAnnouncement— default for in-app notifications on event announcementsnotificationSettings.eventAnnouncementEmail— default for sending event announcement emails
Event announcements: OrgManagementConfig.messaging.eventAnnouncements
enabled— if false, event announcements are disabledallowAnnouncementsDaysBeforeEvent— number of days before event start when announcements are allowed (e.g. 7 = only within 7 days before)includeCheckedIn— if false, only checked-in attendees are eligible (default: true = all registrants)includeAnonymousInEmail— if false, anonymous form registrants are not included in email (default: true when email is resolvable)
Backend routes
Backend routes
All routes under
Create message body:
POST announcement creates an
/org-messages. Auth: verifyToken; event-announcement routes also use requireOrgPermission(ORG_PERMISSIONS.SEND_ANNOUNCEMENTS, 'orgId').General org messages (timeline)
| Method | Path | Description |
|---|---|---|
| POST | /:orgId/messages | Create message |
| GET | /:orgId/messages | List messages (timeline; visibility-filtered) |
| GET | /:orgId/messages/:messageId | Get one message + replies |
| POST | /:orgId/messages/:messageId/like | Like / unlike |
| POST | /:orgId/messages/:messageId/reply | Reply |
| PUT | /:orgId/messages/:messageId | Edit message |
| DELETE | /:orgId/messages/:messageId | Soft-delete message |
content, visibility, optional sendAsOrg, optional parentMessageId (for replies). Server parses content for @event mentions and stores mentionedEvents and links.List messages: Filtered by user’s relationship to org (member → all; follower → members_and_followers + public; public → public only). Only top-level messages (parentMessageId null) and excludes messages with eventId set (event announcements live in the event Communications tab).Event-specific announcements
| Method | Path | Description |
|---|---|---|
| POST | /:orgId/events/:eventId/announcements | Send event announcement (in-app + optional email + optional PDFs) |
| GET | /:orgId/events/:eventId/announcement-recipients | List who would receive (for recipient picker) |
| GET | /:orgId/events/:eventId/announcements | List past announcements for this event |
OrgMessage with eventId set, then:- In-app: Batch template notification
org_event_announcementto selected recipients (members/form registrants with accounts). - Email: For each unique email (attendees, form registrants, resolved anonymous, plus optional
additionalEmails), sends one Resend email with optional PDF attachments.
excludeUserIds, excludeEmails, and optional additionalEmails (max 20). Channels are controlled with channels: { inApp: boolean, email: boolean }.Sending an event announcement
Sending an event announcement
Request bodies
JSON (no attachments):multipart/form-data. Same fields as above as form fields; arrays (excludeUserIds, excludeEmails, additionalEmails, channels) as JSON strings. Attach files with field name attachments (max 3 PDFs, 10MB each).Who receives announcements
- Eligible attendees: From
event.attendees(and optionally only checked-in ifincludeCheckedInis false); author is excluded. - Form registrants: If the event has
registrationFormId,FormResponsedocs withsubmittedByset are included (by user id and email). - Anonymous form respondents: If
includeAnonymousInEmailis true and the event has a registration form, responses withsubmittedBy: nullare included for email only when an email can be resolved (e.g.guestEmailor the answer to the question referenced byevent.notificationEmailQuestionId). - Additional emails: Up to 20 extra addresses via
additionalEmails(email-only recipients).
userId, name, email, and isAnonymous where applicable) plus anonymousWithNoEmailCount for UI.Timing rule
IfallowAnnouncementsDaysBeforeEvent is set (e.g. 7), announcements are only allowed starting that many days before event.start_time. Earlier attempts return 403 with code ANNOUNCEMENTS_NOT_YET_ALLOWED.Email and PDF attachments
Email and PDF attachments
Resend integration
- Client:
Meridian/backend/services/resendClient.js—getResend()returns the Resend SDK instance whenRESEND_API_KEYis set. - Usage: Event announcement emails are sent with
resend.emails.send()per recipient (same HTML, subject, from; optionalattachments). From address uses org name:"Org Name <support@meridian.study>". - HTML: Built by
buildEventAnnouncementEmail()inorgInviteService(org name, event name, start time, author/org picture, message HTML, link to event).
PDF attachments
- Limits: Max 3 attachments per announcement, 10MB per file. Only PDF (
application/pdf) is allowed. - Backend: Multipart requests are handled by a conditional multer middleware (only when
Content-Type: multipart/form-data). Files are validated (type, size, count), then passed to the event announcement service as{ filename, content: Buffer }. The service sanitizes filenames and forwards them to Resend asattachments: [{ filename, content, content_type: 'application/pdf' }]. - Resend: Total email size after Base64 is capped at 40MB per email; 3×10MB stays under that.
- Storage: Attachments are not persisted; they are sent in-memory to Resend only.
Frontend
Frontend
Org timeline
- OrgMessageFeed (e.g. in Club Dashboard): Fetches
GET /org-messages/:orgId/messages, displays timeline. Posting usesPOST /org-messages/:orgId/messages.
Event announcements
- Communications tab (Event Dashboard): Lists past announcements via
GET /org-messages/:orgId/events/:eventId/announcementsand provides a “Send announcement” action. - EventAnnouncementCompose (
Meridian/frontend/src/pages/ClubDash/EventsManagement/components/EventDashboard/EventAnnouncementCompose.jsx):- Fetches recipients:
GET .../announcement-recipients. - Subject, message (markdown), delivery toggles (in-app, email), send-as-org vs as self, recipient picker (include/exclude), optional additional emails (max 20).
- Attachments: “Add PDF” with
accept=".pdf,application/pdf", multiple, max 3 files and 10MB each; selected files shown with remove. On send, if there are attachments the client buildsFormDataand POSTs multipart; otherwise POSTs JSON.
- Fetches recipients:
Security and limits
Security and limits
- Auth: All routes require a valid token. Event announcement routes require org permission
SEND_ANNOUNCEMENTS. - Org state: Messaging must be enabled (
messageSettings.enabled). If the org isapprovalStatus: 'pending', posting may be blocked unless allowed bypendingOrgLimits.allowedActions(e.g.post_messages). - Content: Min/max character limits and optional profanity filter apply. Event announcement content is required and validated the same way.
- Attachments: Type (PDF only), count (≤3), and size (≤10MB each) are enforced on the server; filenames are sanitized (path stripped, length capped, .pdf ensured).
Common pitfalls
Common pitfalls