Skip to main content
The org messages system lets organizations communicate with members and event attendees through org-wide timeline messages and event-specific announcements. Event announcements can be delivered in-app and by email (via Resend), with optional PDF attachments.

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
Org messaging is stored as OrgMessage documents (collection: orgMessages).

Core fields

FieldTypeDescription
orgIdObjectIdOrganization (ref: Org)
authorIdObjectIdAuthor (ref: User)
contentStringMessage body; max length from org/system config (hard cap 2000)
visibilityStringmembers_only | members_and_followers | public
mentionedEventsObjectId[]Events referenced in content (from @event parsing)
linksString[]Parsed links from content
likesObjectId[]User IDs who liked
likeCountNumberDenormalized like count
parentMessageIdObjectIdSet for replies (ref: OrgMessage)
replyCountNumberDenormalized reply count
isDeletedBooleanSoft delete flag
deletedAtDateWhen soft-deleted

Event-announcement fields

When the message is an event-specific announcement (eventId set), these apply:
FieldTypeDescription
eventIdObjectIdEvent (ref: Event); only org-hosted events
subjectStringOptional email subject (max 200 chars)
sendAsOrgBooleanIf true, display org name/avatar instead of author
Event announcements are excluded from the org timeline; they appear only in the event’s Communications tab.

Per-org: Org.messageSettings

org.messageSettings = {
  enabled: Boolean,
  postingPermissions: [String],  // e.g. ['owner', 'admin', 'officer']
  visibility: 'members_only' | 'members_and_followers' | 'public',
  characterLimit: Number,
  allowLikes: Boolean,
  allowReplies: Boolean
}
  • Effective character limit = Math.min(org.messageSettings.characterLimit, systemConfig.messaging.maxCharacterLimit).

Global: OrgManagementConfig.messaging

  • maxCharacterLimit — hard ceiling
  • minCharacterLimit — hard floor
  • requireProfanityFilter — run content through profanity filter before save/send
  • notificationSettings.notifyOnEventAnnouncement — default for in-app notifications on event announcements
  • notificationSettings.eventAnnouncementEmail — default for sending event announcement emails

Event announcements: OrgManagementConfig.messaging.eventAnnouncements

  • enabled — if false, event announcements are disabled
  • allowAnnouncementsDaysBeforeEvent — 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)
All routes under /org-messages. Auth: verifyToken; event-announcement routes also use requireOrgPermission(ORG_PERMISSIONS.SEND_ANNOUNCEMENTS, 'orgId').

General org messages (timeline)

MethodPathDescription
POST/:orgId/messagesCreate message
GET/:orgId/messagesList messages (timeline; visibility-filtered)
GET/:orgId/messages/:messageIdGet one message + replies
POST/:orgId/messages/:messageId/likeLike / unlike
POST/:orgId/messages/:messageId/replyReply
PUT/:orgId/messages/:messageIdEdit message
DELETE/:orgId/messages/:messageIdSoft-delete message
Create message body: 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

MethodPathDescription
POST/:orgId/events/:eventId/announcementsSend event announcement (in-app + optional email + optional PDFs)
GET/:orgId/events/:eventId/announcement-recipientsList who would receive (for recipient picker)
GET/:orgId/events/:eventId/announcementsList past announcements for this event
POST announcement creates an OrgMessage with eventId set, then:
  1. In-app: Batch template notification org_event_announcement to selected recipients (members/form registrants with accounts).
  2. Email: For each unique email (attendees, form registrants, resolved anonymous, plus optional additionalEmails), sends one Resend email with optional PDF attachments.
Recipients can be narrowed with excludeUserIds, excludeEmails, and optional additionalEmails (max 20). Channels are controlled with channels: { inApp: boolean, email: boolean }.

Request bodies

JSON (no attachments):
POST /org-messages/:orgId/events/:eventId/announcements
Content-Type: application/json
Authorization: Bearer <token>

{
  "content": "Reminder: bring your ID for check-in.",
  "subject": "Reminder for Saturday's meetup",
  "sendAsOrg": true,
  "excludeUserIds": [],
  "excludeEmails": [],
  "additionalEmails": ["guest@example.com"],
  "channels": { "inApp": true, "email": true }
}
Multipart (with PDF attachments):Use 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).
POST /org-messages/:orgId/events/:eventId/announcements
Content-Type: multipart/form-data; boundary=----...
Authorization: Bearer <token>

------...
Content-Disposition: form-data; name="content"
Reminder: see the attached agenda.
------...
Content-Disposition: form-data; name="subject"
Agenda for Saturday
------...
Content-Disposition: form-data; name="attachments"; filename="agenda.pdf"
Content-Type: application/pdf
<binary>
------...
{
  "success": true,
  "message": "Event announcement sent successfully",
  "data": { "_id": "...", "orgId": "...", "eventId": "...", "content": "...", "subject": "...", "authorId": { "name": "...", "username": "...", "picture": "..." }, ... }
}

Who receives announcements

  • Eligible attendees: From event.attendees (and optionally only checked-in if includeCheckedIn is false); author is excluded.
  • Form registrants: If the event has registrationFormId, FormResponse docs with submittedBy set are included (by user id and email).
  • Anonymous form respondents: If includeAnonymousInEmail is true and the event has a registration form, responses with submittedBy: null are included for email only when an email can be resolved (e.g. guestEmail or the answer to the question referenced by event.notificationEmailQuestionId).
  • Additional emails: Up to 20 extra addresses via additionalEmails (email-only recipients).
The GET announcement-recipients endpoint returns the full list (with userId, name, email, and isAnonymous where applicable) plus anonymousWithNoEmailCount for UI.

Timing rule

If allowAnnouncementsDaysBeforeEvent 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.

Resend integration

  • Client: Meridian/backend/services/resendClient.jsgetResend() returns the Resend SDK instance when RESEND_API_KEY is set.
  • Usage: Event announcement emails are sent with resend.emails.send() per recipient (same HTML, subject, from; optional attachments). From address uses org name: "Org Name <support@meridian.study>".
  • HTML: Built by buildEventAnnouncementEmail() in orgInviteService (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 as attachments: [{ 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.
If RESEND_API_KEY is missing, announcement emails (and any attachments) are skipped; the OrgMessage and in-app notifications are still created.

Org timeline

  • OrgMessageFeed (e.g. in Club Dashboard): Fetches GET /org-messages/:orgId/messages, displays timeline. Posting uses POST /org-messages/:orgId/messages.

Event announcements

  • Communications tab (Event Dashboard): Lists past announcements via GET /org-messages/:orgId/events/:eventId/announcements and 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 builds FormData and POSTs multipart; otherwise POSTs JSON.
  • 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 is approvalStatus: 'pending', posting may be blocked unless allowed by pendingOrgLimits.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).
Min character limit: System config can reject short messages; ensure minCharacterLimit matches product expectations.
Posting permissions: Uses role names (e.g. owner, admin, officer). Renaming roles requires updating postingPermissions.
Event announcements vs timeline: Messages with eventId set do not appear in the org message timeline; they only appear under the event’s Communications tab.
Anonymous recipients: To email anonymous form respondents, configure a notification-email question in registration settings and keep includeAnonymousInEmail enabled. Guests with no resolvable email are counted in anonymousWithNoEmailCount and cannot receive email.
Development: In NODE_ENV=development, announcement emails and in-app notifications may be redirected to a single dev address (e.g. james@activeherb.com); check eventAnnouncementService.js for the redirect logic.