TradeForce migration docs

#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.

flowchart TB subgraph shell["Cabinet shell"] Header["App header"] SideBar["Role side bar"] end subgraph page["Messages page"] List["Conversation list"] Thread["Thread + composer"] NewChat["New-chat search"] end subgraph data["Persistence and email"] Chats["chats"] Messages["messages"] Notes["notifications"] PM["PostMark edge"] end Header --> page SideBar --> page List --> Chats NewChat --> Chats Thread --> Messages Thread --> Notes Thread --> PM Messages --> Chats

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

  1. A signed-in contractor or company opens /chat; anonymous users and roles that are neither contractor nor company redirect to /.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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_ids on that thread’s messages that do not already include them (soft-hide for self).

#J2 — Send a message, notify the peer, and mark read

  1. With a chat loaded for ?user=, the thread lists messages for that chat where the current user is not in deleted_for_ids, oldest first; “No Messages” shows when the message list has no visible rows.
  2. 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).
  3. 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.
  4. 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.
  5. 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).

Generated from the repository at 2026-09-22 15:55 UTC · source of truth is docs/ in the workspace, not this site.