#Messaging and notifications
Module id: MOD-CHAT
Doc profile: capability
Signed-in contractors and companies open /chat under the cabinet shell to list conversations, start a thread with a shift-related partner, exchange text and file attachments, and notify the peer with an in-app notification plus a PostMark template email. Target stack is React routes with Supabase chats / messages (and shared notifications), storage for attachments, and a server-side PostMark edge function. Bubble chat_page workflows are the behavior evidence, not the runtime.
#Purpose
Companies and contractors need a private 1:1 inbox so they can discuss shifts without leaving TradeForce. This module owns the Messages page (conversation list + thread + composer), chat/message persistence and RLS, soft-hide of a thread for self, mark-read on open, create-on-send notification rows, and the PostMark “New message” email. It does not own header chrome or the notification bell panel (MOD-SHELL), auth/session (MOD-AUTH), or other PostMark templates (canceled shifts, time-tracking, payment cycle).
#Scope boundaries
| In scope | Out of scope |
|---|---|
/chat and /chat?user= under cabinet shell |
Header bell list / mark-read panel (SHELL-HEADER-ACTIONS) |
| Conversation list, new-chat search from shift partners, soft-hide thread | Global user directory or admin chat access |
| Message thread, composer, attachments, mark-read | Hard-deleting chat rows for both parties |
chats / messages schema + participant RLS; reuse notifications |
Recreating notifications (owned by SHELL-HEADER-ACTIONS) |
| PostMark New message edge (secret server-side) | Other PostMark calls on namespace (canceled shifts, time report, payment cycle) |
| Empty copy “No Chats” / “No Messages”; title “Messages” | Header shortcut query message=unapproved (shell nav only; this page reads user) |
| Chat CustomDefinition as evidence-only empty shell | Mounting that empty reusable as a separate UI surface |
#Actors
| Actor | Behavior in this module |
|---|---|
| Contractor (signed in) | Opens Messages; lists chats where they are a participant; starts or opens a thread with a company partner from their shifts; sends text/files; receives peer notifications and email. |
| Company (signed in) | Same inbox/thread journeys with contractor partners from shift relationships. |
| Admin / other roles | PageLoaded redirects to /; never sees inbox chrome. |
| Anonymous | PageLoaded redirects to /; no chat UI. |
| Session client | SPA Supabase client for chats/messages/notifications/storage; PostMark token only on the edge function. |
#Capability composition
Chat is one cabinet host page: Header + SideBar from MOD-SHELL around a Messages body. The left pane lists participant chats; the right pane shows the thread for URL user when a chat contains both that peer and the current user. Send writes a message, links it on the chat, inserts a notification for the peer, then calls PostMark. Soft-delete only adds the current user to each message’s deleted-for list. Schema and the PostMark edge are the foundation slice underneath the UI journeys.
Bubble evidence for composition is under Traceability / IR evidence. The Chat element definition root is empty; UI and workflows live on the chat page root.
#Primary journeys
Journeys are reconstructed from chat_page PageLoaded and ButtonClicked / InputChanged workflows (gates → list/create → send → notify → mark-read). The Chat CustomDefinition contributes no separate journey surface.
#J1 — Open Messages, find or start a chat, hide a thread
- A signed-in contractor or company opens
/chat; anonymous users and roles that are neither contractor nor company redirect to/. - The page shows title “Messages”, a conversation list of chats whose participants include the current user, and empty copy “No Chats” when that list has no visible rows.
- The user opens new-chat search; the focus list shows unique company partners from the user’s shifts, filtered by the Search input on full-name search text.
- Choosing a partner: if a chat with both participants already exists, navigate to
/chat?user={peerId}; if none exists, create a chat with both users on participant and visible-participant lists, then close/reset the focus group (no automatic?user=navigation on create). - Selecting a conversation row sets
?user=to the other participant, marks unread messages authored by that peer as read when any exist, and scrolls the thread to the latest message. - Edit opens the delete-chat confirm popup (“Please confirm your action” / “Do you really want to delete this chat?”); Discard closes with no writes; Confirm adds the current user to
deleted_for_idson that thread’s messages that do not already include them (soft-hide for self).
#J2 — Send a message, notify the peer, and mark read
- With a chat loaded for
?user=, the thread lists messages for that chat where the current user is not indeleted_for_ids, oldest first; “No Messages” shows when the message list has no visible rows. - The user types in Enter Message, optionally attaches files via the uploader (pending files live on the composer custom file list; preview close removes and deletes the upload).
- Send creates a message (body, unread, recipient = peer, attachments from the composer list), creates a notification whose body is exactly “You have a new message waiting. ” for the peer, links the message onto the chat, invokes PostMark New message (From amanda.gasman@tradeforce.dev, To = peer email, TemplateAlias user-invitation-1, empty TemplateModel), resets inputs, and scrolls to the new message.
- PostMark runs only through the server edge (no browser token). Email is eventual after the DB write; a transport failure must surface clearly without inventing a silent rollback unless implement documents compensating UX.
- When the thread pane has chat data on load, after a short pause the app scrolls to the last message and sets read on messages in that chat addressed to the current user that are still unread.
#Vertical slices (decompose)
| Item | Product outcome |
|---|---|
| CHAT-FOUNDATION | chats / messages migrations + participant/creator-or-recipient RLS; chat attachment storage; PostMark new-message edge; typed SPA helpers. Infrastructure enabler (no standalone UI journey). |
| CHAT-INBOX | /chat Messages UI: gates, conversation list, empty states, new-chat from shifts, create-vs-select, soft-hide delete popup. |
| CHAT-SEND-NOTIFY | Thread list/composer, attachments, send + notification + PostMark, mark-read on open/select. |
#Key UI states
| State | When | User sees |
|---|---|---|
| Signed-out / forbidden role | PageLoaded gate | Redirect to /; no Messages chrome |
| Empty inbox | No participant chats visible | “No Chats” |
| Empty thread | No visible messages for loaded chat / no user chat |
“No Messages” |
| Thread selected | ?user= matches a two-party chat |
Peer thread + composer |
| New-chat focus open | Search control | Partner list filtered by Search |
| Delete confirm | Edit on thread | Confirm / Discard popup copy as above |
| Unread → read | Open list row or load thread | Peer-authored or recipient unread flipped |
#Style and fidelity
Provisional IR style for chat regions; shared tokens come from the app-level stylesheet that SHELL-LAYOUT (MOD-SHELL) delivers out of IR styles (ir/slices/styles____part_all.json — 49 styles, 8 color tokens, 1 font token); this cycle's module map has no separate design-system module, so do not defer a token to one. Stamp data-bubble-id on the page root and on migrated list/thread/composer roots used in parity checks. Transcribe title and empty/delete copy exactly. Compose with SHELL-LAYOUT Header/SideBar; do not reimplement chrome. Do not mount the empty Chat CustomDefinition as a reusable.
#Failure modes
| Case | Expected behavior |
|---|---|
Anonymous or non contractor/company on /chat |
Redirect to /; no inbox content. |
| Chat/message list or create failure | User-visible inline/page error; no silent empty success. |
| Send / upload / mark-read failure | Plain-language error; no uncaught promise. |
| PostMark edge failure after DB write | Clear UI error; message may already persist (eventual email). |
| Non-participant tries to read chat/message | RLS denies; UI shows error / empty, never another user’s thread. |
| Soft-hide Confirm | Only current user added to deleted-for; peer still sees messages. |
Header opens /chat?message=unapproved |
Chat page ignores message; only user drives the thread pane. |
#Traceability
| Product concept | Target (route / component) | Bubble evidence |
|---|---|---|
| Messages page | /chat, ChatPage |
bTJQD0 |
| Empty Chat CustomDefinition | Evidence only (do not mount) | bTJQB0 |
| Messages title | Title region | bTJQP0 |
| Conversation list | ConversationList |
bTJUu0, bTJVR0 |
| Thread pane | MessageThread |
bTJUt0, bTJSw0 |
| Composer | MessageComposer |
bTJTz0, bTJUD0, bTJUF0, bTJUJ0 |
| New-chat focus | NewChatFocus |
bTMee, bTMeq, bTMnM |
| Delete chat popup | DeleteChatDialog |
bTMjL, bTMjp, bTMjh |
| PageLoaded anonymous redirect | Guard → / |
bTMfx, bTMfz |
| PageLoaded role redirect | Guard → / |
bTMgD, bTMgF |
| Select conversation | URL user + mark peer unread |
bTMeX, bTMiO, bTMiP |
| Create chat when none | Insert chats |
bTMgV, bTMgd |
| Navigate when chat exists | /chat?user= |
bTMgK, bTMqj0 |
| Soft-hide messages | deleted_for_ids |
bTMjp, bTMjv |
| Send message | Insert messages + link chat |
bTMfT, bTMfZ, bTMgh |
| In-app notification on send | notifications row |
bTNLX, user_types/notification |
| PostMark New message | Edge postmark-new-message |
bTNMr, external_call:bTNMZ:bTNMn, external_ns:bTNMZ |
| Mark read on thread load | PATCH messages read | bTMhF, bTMis |
| Chat / message types + privacy | chats, messages RLS |
chat, message, privacy_role:chat:user_, privacy_role:message:user_ |
| Empty states | Copy conditions | element_condition:bTJUz0:1, element_condition:bTJUK0:0 |
| Composer pending files | Custom state | custom_state:bTJTz0:file_ |
| Header / SideBar embeds | Compose with MOD-SHELL | bTJQH0, bTJQI0 |
#IR evidence
Dense ids for implement and review. Prefer catalog refs below; Bubble path key for the page pointer is under /pages/bTJSr0 (entity bTJQD0). Slice ir/slices/bTJSr0____root.json indexes page elements/workflows; ir/slices/edef_bTJQC0____root.json is the thin Chat ED (bTJQB0).
- Module evidence roots:
bTJQD0,bTJQB0 - Evidence segments:
ir/slices/bTJSr0____root.json,ir/slices/edef_bTJQC0____root.json,chat,message,user_types/notification,external_ns:bTNMZ - Data types / fields:
chat,message,user_types/notification,user_list_user,visible_user_list_user,message_list_custom_message,chat_custom_chat,text_text,read_boolean,recipient_user,delete_list_user,file_list_file - Privacy:
privacy_role:chat:user_(logged-in participant),privacy_role:message:user_(creator or recipient) - Workflows (catalog refs):
bTMfx,bTMgD,bTMnM,bTMgV,bTMgK,bTMeX,bTMjp,bTMjh,bTMfT,bTMhF - Actions:
bTMfz,bTMgF,bTMgd,bTMqj0,bTMiO,bTMiP,bTMjv,bTMfZ,bTNLX,bTMgh,bTNMr,bTMis - UI anchors:
bTJQP0,bTJUu0,bTJVR0,bTJUt0,bTJSw0,bTJTz0,bTJUD0,bTJUF0,bTJUJ0,bTMee,bTMeq,bTMjL,bTJUz0,bTJUK0,bTJUh0 - Integrations:
external_ns:bTNMZ,external_call:bTNMZ:bTNMn(withTemplate; shared headers private on namespace) - Decompose:
CHAT-FOUNDATION,CHAT-INBOX,CHAT-SEND-NOTIFY - Depends on: AUTH-FOUNDATION, SHELL-LAYOUT, SHELL-HEADER-ACTIONS (
notificationstable) - Not this module:
external_call:bTNMZ:bTNMl(Canceled shifts),external_call:bTNMZ:bTNMs(time-tracking report),external_call:bTNMZ:bTNMt(payment cycle); option_setnotificationupload/delete copy; deleted fieldfile_file