Skip to main content

Chat

What it is​

In-app messaging scoped to an arbitrary "interest" (an event's planning chat, an event's full guest chat, a vendor-event thread, etc.), delivered over both a REST API and a WebSocket gateway for real-time push.

Core concepts​

  • interestId: the format entityType:entityId (e.g. event:242, event_guests:242, vendor_event:55) is the single mechanism that scopes every chat — there's no separate "conversation" or "room" entity to create first. The prefix determines who's allowed in.
  • Pattern-based permissions, default-open: access rules are only defined for specific entityType prefixes. Any interestId whose prefix has no rule is open to any authenticated user — this is a deliberate default, not an oversight, so treat adding a new prefix's permission check as a required step whenever a new "kind" of chat is introduced, not an optional hardening pass.
  • REST + WebSocket split: sending/fetching/searching/deleting messages goes through the REST API (/v1/chat/:interestId/messages*); the ChatWebSocketGateway (initialized alongside the HTTP server in app.ts) pushes messages to connected clients in real time. The REST API is the source of truth; the WebSocket layer is a delivery mechanism on top of it.

How it works​

Current permission patterns​

interestId prefixWho can access
event:{eventId}Only the event creator
event_guests:{eventId}Planners (via the planners table) + guests with rsvpStatus = 'accepted'
vendor_event:{vendorId}The vendor owner, vendor team members (vendor_collaborators, keyed by the vendor's profileId), and event planners for the vendor's event
anything elseAny authenticated user (default)

Permission is checked on every operation — send, list, get one, get-after/before, search, count, replies, delete — not just on first access, in ChatPermissionsService.canAccessChat / canSendMessage.

Adding a new pattern​

The permission system is a switch on the parsed entityType in chat-permissions.service.ts — adding a new chat "kind" means adding a case there (and to the pattern list used by hasAccessRules), not changing any routes.

Key business rules & edge cases​

  • Fails closed on error: if a permission check throws (e.g. a DB error resolving the entity), the code denies access rather than allowing it.
  • Inconsistent error status on denial: a denied send returns 403; a denied read currently surfaces as 500 (the permission check throws inside a call path that isn't specifically caught as a 403 for reads). Don't rely on status code alone to distinguish "not permitted" from "actually broken" on read endpoints — check the message.
  • vendor_event access is keyed off vendor.profileId, not the event-vendor row's own id — a vendor's team members are linked via vendor_collaborators.vendorId = vendor.profileId, which is easy to get backwards when adding a new integration.

GET/POST /v1/chat/{interestId}/messages, GET .../messages/count, GET .../messages/search, GET .../messages/after/{messageId}, GET .../messages/before/{messageId}, GET .../messages/{messageId}, GET .../messages/{messageId}/replies, DELETE .../messages/{messageId}. Not yet in the generated OpenAPI spec.

  • src/modules/chat/chat.routes.ts, chat.controller.ts, chat.service.ts
  • src/modules/chat/chat-permissions.service.ts — all access-control logic lives here
  • src/modules/chat/chat-websocket-init.ts, chat-websocket.gateway.ts — real-time delivery