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 formatentityType: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
entityTypeprefixes. AnyinterestIdwhose 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*); theChatWebSocketGateway(initialized alongside the HTTP server inapp.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 prefix | Who 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 else | Any 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 as500(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_eventaccess is keyed offvendor.profileId, not the event-vendor row's ownid— a vendor's team members are linked viavendor_collaborators.vendorId = vendor.profileId, which is easy to get backwards when adding a new integration.
Related endpoints
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.
Related code
src/modules/chat/chat.routes.ts,chat.controller.ts,chat.service.tssrc/modules/chat/chat-permissions.service.ts— all access-control logic lives heresrc/modules/chat/chat-websocket-init.ts,chat-websocket.gateway.ts— real-time delivery