- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services - Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history - Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history - Control center: free trial, extension timeout, early end config toggles - DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20 KiB
Phase 3 Implementation Plan: Chat Engine
Summary of Clarified Requirements
| Topic | Decision |
|---|---|
| Chat opening | Time/price selection dialog before pairing |
| Price tiers (mock) | 15min/30k, 30min/60k, 45min/100k, 60min/150k, 24jam/250k |
| Free trial | One-time per customer; enabled/disabled globally via Control Center |
| Free trial duration | Single global config value (minutes) in Control Center |
| Payment timing | After pairing starts, but mocked in this phase |
| Chat transport | WebSocket (real-time) + FCM (background push notifications) |
| Message types | Text-only now; schema supports image/voice/video later |
| Emoji | Works natively (unicode text), no special handling needed |
| Message status | Sent (server ack), Delivered (client ack), Read (client opened) |
| Typing indicator | 3-second timeout, throttled (send at most once per 2-3s) |
| Chat history storage | Backend API as source of truth; no local cache |
| Session timer | Backend-authoritative (server-side countdown) |
| Timer warning | Show remaining time at 1 minute left |
| Extension timeout | 1 minute for customer to decide + mitra to confirm (configurable) |
| Chat during extension | Paused — no messages until extension confirmed or rejected |
| Early end | Mechanism built, disabled by default, configurable per role |
| Closing message | Free-text goodbye message from both parties |
| Chat history view | Full read-only transcript, kept forever |
| Deletion requests | Deferred to later phase |
| Sessions per customer | One active session at a time |
| WSS termination | Cloud Run handles TLS; backend uses plain ws:// |
| Control center transcripts | Viewable by certain roles only (deferred to later phase) |
| Notification permission | Request on first app launch |
1. Database Changes
1.1 New table: chat_messages
Stores all chat messages.
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
|
session_id |
INT REFERENCES chat_sessions(id) |
|
sender_type |
VARCHAR |
'customer' or 'mitra' |
sender_id |
INT |
customer or mitra ID |
type |
VARCHAR DEFAULT 'text' |
'text' now; 'image', 'voice', 'video' later |
content |
TEXT |
Message text (or file URL for future media) |
metadata |
JSONB |
Nullable; for future media (file size, duration, thumbnail) |
status |
VARCHAR DEFAULT 'sent' |
'sent', 'delivered', 'read' |
delivered_at |
TIMESTAMPTZ |
When recipient's client acknowledged |
read_at |
TIMESTAMPTZ |
When recipient opened/read the message |
created_at |
TIMESTAMPTZ DEFAULT now() |
Indexes:
(session_id, created_at)— fetch messages in chronological order(session_id, status)— query undelivered/unread messages
1.2 New table: session_closures
Stores goodbye messages when a session ends.
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
|
session_id |
INT REFERENCES chat_sessions(id) |
|
user_type |
VARCHAR |
'customer' or 'mitra' |
user_id |
INT |
customer or mitra ID |
message |
TEXT |
Free-text goodbye message |
created_at |
TIMESTAMPTZ DEFAULT now() |
1.3 New table: session_extensions
Tracks extension requests and their outcomes.
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
|
session_id |
INT REFERENCES chat_sessions(id) |
|
requested_duration_minutes |
INT |
Duration customer selected |
requested_price |
INT |
Mock price (in IDR) |
status |
VARCHAR |
'pending', 'accepted', 'rejected', 'timeout' |
requested_at |
TIMESTAMPTZ DEFAULT now() |
|
responded_at |
TIMESTAMPTZ |
1.4 Alter table: chat_sessions
Add columns to support timed sessions:
| Column | Type | Notes |
|---|---|---|
duration_minutes |
INT |
Selected duration (15/30/45/60/1440) |
price |
INT |
Mock price in IDR (0 for free trial) |
is_free_trial |
BOOLEAN DEFAULT false |
|
expires_at |
TIMESTAMPTZ |
Computed: paired_at + duration_minutes |
extended_minutes |
INT DEFAULT 0 |
Total extended time |
Add new session statuses:
extending— customer requested extension, waiting for mitra confirmationclosing— session ended, waiting for goodbye messages
1.5 New table: customer_transactions
Tracks whether a customer has had any transaction (for free trial eligibility).
| Column | Type | Notes |
|---|---|---|
id |
SERIAL PRIMARY KEY |
|
customer_id |
INT REFERENCES customers(id) |
|
session_id |
INT REFERENCES chat_sessions(id) |
|
type |
VARCHAR |
'free_trial', 'paid', 'extension' |
amount |
INT |
0 for free trial |
created_at |
TIMESTAMPTZ DEFAULT now() |
1.6 Extend app_config
New config keys:
| Key | Value (JSONB) | Purpose |
|---|---|---|
free_trial_enabled |
{ "value": true } |
Enable/disable free trial globally |
free_trial_duration_minutes |
{ "value": 5 } |
Free trial session duration |
extension_timeout_seconds |
{ "value": 60 } |
Time limit for extension negotiation |
early_end_mitra_enabled |
{ "value": false } |
Allow mitra to end session early |
early_end_customer_enabled |
{ "value": false } |
Allow customer to end session early |
2. Backend Changes
2.1 WebSocket Setup
- Add
@fastify/websocketplugin (src/plugins/websocket.js) - Single WebSocket endpoint:
GET /api/shared/ws - Connection authenticated via Firebase token (sent as query param or first message)
- After auth, server identifies user as customer or mitra and joins them to their session channel
- Valkey pub/sub remains the backend message bus; WebSocket is the client-facing transport
- Architecture:
Client ↔ WebSocket ↔ Backend ↔ Valkey pub/sub ↔ Backend ↔ WebSocket ↔ Other client
WebSocket message types (JSON):
| Type | Direction | Purpose |
|---|---|---|
auth |
Client → Server | Authenticate with Firebase token |
auth_ok |
Server → Client | Authentication successful |
message |
Client → Server | Send a chat message |
message |
Server → Client | Receive a chat message |
message_status |
Server → Client | Delivery/read status update |
message_ack |
Server → Client | Server acknowledges sent message (sent status) |
typing |
Client → Server | User is typing |
typing |
Server → Client | Other user is typing |
session_timer |
Server → Client | Timer warning (1 min left) |
session_expired |
Server → Client | Session time is up |
extension_request |
Server → Client | Extension request notification (to mitra) |
extension_response |
Server → Client | Extension accepted/rejected (to customer) |
session_paused |
Server → Client | Chat paused during extension negotiation |
session_resumed |
Server → Client | Chat resumed after extension accepted |
session_closing |
Server → Client | Session ending, prompt for goodbye message |
early_end |
Client → Server | Request to end session early |
delivered |
Client → Server | Client acknowledges message delivery |
read |
Client → Server | Client marks messages as read |
2.2 FCM Push Notification Setup
- Use existing
firebase-adminplugin for sending push notifications - Store FCM device token per user (new columns on
customersandmitrastables) - Send push notification when recipient's WebSocket is not connected
- FCM payload includes: message preview, session ID, sender name
- On notification tap: deep-link to specific chat screen
New columns:
| Table | Column | Type |
|---|---|---|
customers |
fcm_token |
VARCHAR |
mitras |
fcm_token |
VARCHAR |
New endpoint:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/shared/device-token |
Register/update FCM device token |
2.3 New Public Routes — Chat Opening
| Method | Path | Purpose |
|---|---|---|
GET |
/api/client/chat/pricing |
Get mock price tiers + free trial eligibility |
POST |
/api/client/chat/request |
Start pairing with selected duration/price (updated from Phase 2) |
2.4 New Public Routes — Chat Messages
| Method | Path | Purpose |
|---|---|---|
GET |
/api/shared/chat/:sessionId/messages |
Fetch message history (paginated, for reconnect) |
GET |
/api/shared/chat/:sessionId/info |
Get session info (timer, status, participants) |
2.5 New Public Routes — Session Closure
| Method | Path | Purpose |
|---|---|---|
POST |
/api/client/sessions/:sessionId/extend |
Customer requests extension with selected duration |
POST |
/api/mitra/sessions/:sessionId/extend-response |
Mitra accepts/rejects extension |
POST |
/api/shared/sessions/:sessionId/close-message |
Submit goodbye message |
2.6 New Public Routes — Chat History
| Method | Path | Purpose |
|---|---|---|
GET |
/api/client/chat/history |
List past sessions (with mitra name, closure messages) |
GET |
/api/mitra/chat/history |
List past sessions (with customer name, closure messages) |
GET |
/api/shared/chat/:sessionId/transcript |
Full read-only chat transcript |
2.7 New Internal Routes — Control Center
| Method | Path | Purpose |
|---|---|---|
GET |
/internal/config/free-trial |
Get free trial config |
PATCH |
/internal/config/free-trial |
Update free trial enabled + duration |
GET |
/internal/config/extension-timeout |
Get extension timeout config |
PATCH |
/internal/config/extension-timeout |
Update extension timeout |
GET |
/internal/config/early-end |
Get early end config |
PATCH |
/internal/config/early-end |
Update early end per role |
2.8 New Services
| Service | Responsibilities |
|---|---|
chat.service.js |
Send message, update delivery/read status, fetch message history, typing event relay |
session-timer.service.js |
Backend-authoritative countdown, expiry check, 1-min warning trigger, extension timeout |
extension.service.js |
Extension request/response flow, session pause/resume, timeout handling |
closure.service.js |
Goodbye message submission, session completion, transaction recording |
notification.service.js |
FCM push notification sending, device token management, online/offline detection |
pricing.service.js |
Mock price tiers, free trial eligibility check |
2.9 Chat Flow (Backend Detail)
Chat Opening:
1. Customer opens "Mulai Curhat"
2. Backend returns pricing tiers + free trial eligibility
- Free trial shown if: free_trial_enabled AND customer has 0 records in customer_transactions
3. Customer selects duration/price (or free trial)
4. POST /api/client/chat/request with { duration_minutes, price, is_free_trial }
5. Backend creates chat_session with duration_minutes, price, is_free_trial
6. Existing Phase 2 pairing flow proceeds (blast, accept, etc.)
7. On successful pairing:
- Set expires_at = now() + duration_minutes
- Create customer_transactions record
- Start server-side timer
Chat Messaging:
1. Both parties connect via WebSocket after pairing
2. Client sends { type: 'message', content: '...' }
3. Backend saves to chat_messages (status: 'sent'), publishes via Valkey
4. Backend sends message_ack to sender (sent ✓)
5. Recipient's WebSocket receives message
6. Recipient sends { type: 'delivered' } → backend updates status, notifies sender (delivered ✓✓)
7. Recipient views message → sends { type: 'read' } → backend updates status (read ✓✓ blue)
8. If recipient offline → backend sends FCM push notification instead
Session Expiry:
1. Backend timer fires 1 min before expires_at
2. Send session_timer to both clients via WebSocket
3. Both apps show countdown timer
4. At expires_at, backend sends session_expired
5. Customer gets extend/close dialog
6. If extend: POST /api/client/sessions/:id/extend
- Session status → extending, chat paused
- Mitra gets extension_request via WebSocket
- Mitra accepts/rejects within timeout (default 60s)
- If accept: extend expires_at, resume chat
- If reject or timeout: proceed to closure
7. If close (or after rejected extension):
- Session status → closing
- Both parties submit goodbye message
- Session status → completed
Early End (when enabled):
1. User sends { type: 'early_end' } via WebSocket
2. Backend checks if early end is enabled for that role
3. If enabled: skip to closure flow (step 7 above)
4. If disabled: reject with error
2.10 Typing Indicator (Backend Detail)
1. Client sends { type: 'typing' } via WebSocket
2. Backend relays to other party via Valkey pub/sub → WebSocket
3. Receiving client shows typing indicator
4. Receiving client auto-hides after 3 seconds of no new typing event
5. Sending client throttles: at most one typing event per 2 seconds
3. Client App Changes
3.1 New BLoC: ChatBloc
Manages active chat messaging.
Events: ConnectWebSocket, DisconnectWebSocket, SendMessage, MessageReceived, MessageStatusUpdate, TypingStarted, TypingStopped, SessionTimerWarning, SessionExpired
States: ChatInitial, ChatConnecting, ChatConnected(messages), ChatTimerWarning(remaining), ChatSessionExpired, ChatError
- On
ConnectWebSocket→ authenticate, load message history from API, listen for incoming messages - On
SendMessage→ send via WebSocket, add to local message list with "sending" state - On
MessageReceived→ add to list, send delivery acknowledgment - On
MessageStatusUpdate→ update message status (sent → delivered → read)
3.2 New BLoC: ChatOpeningBloc
Manages pricing selection and free trial.
Events: LoadPricing, SelectTier, SelectFreeTrial, ConfirmSelection
States: PricingLoading, PricingLoaded(tiers, freeTrialEligible), TierSelected(tier), PricingError
- On
LoadPricing→ call/api/client/chat/pricing - On
ConfirmSelection→ trigger existingPairingBloc.RequestPairingwith duration/price
3.3 New BLoC: SessionClosureBloc
Manages extension and goodbye flow.
Events: SessionExpired, RequestExtension, ExtensionResult, SubmitGoodbye
States: ClosureInitial, ShowExtendDialog, ExtendingWaitingMitra, ExtensionAccepted, ExtensionRejected, ShowGoodbyeInput, ClosureComplete
3.4 Screen Changes
| Screen | Changes |
|---|---|
| Home screen | "Mulai Curhat" opens pricing dialog instead of directly pairing |
| New: Pricing dialog | Bottom sheet with 5 price tiers + free trial option (if eligible) |
| New: Chat screen | Full chat UI: message list, text input, send button, typing indicator |
| Chat screen | Message bubbles with status icons (✓ sent, ✓✓ delivered, ✓✓ blue read) |
| Chat screen | Countdown timer overlay at 1 minute remaining |
| New: Extension dialog | "Extend session?" with price tier selection |
| New: Waiting mitra dialog | "Menunggu konfirmasi Bestie..." with timeout |
| New: Goodbye screen | Free-text input for closing message |
| New: Chat history list | List of past sessions (bestie name, date, goodbye message) |
| New: Chat transcript screen | Read-only scrollable chat history |
3.5 Navigation Updates
New routes in GoRouter:
/chat/pricing— pricing selection dialog/screen/chat/session/:sessionId— active chat screen (updated from Phase 2)/chat/history— chat history list/chat/history/:sessionId— read-only transcript
3.6 FCM Setup
- Request notification permission on first app launch
- Register device token via
POST /api/shared/device-token - Handle incoming notifications: tap → navigate to specific chat screen
- Update token on app launch (tokens can rotate)
4. Mitra App Changes
4.1 New BLoC: ChatBloc
Same as client app — manages active chat messaging. Shared message type structure.
Events/States: Same as client app ChatBloc.
4.2 New BLoC: ExtensionBloc
Handles incoming extension requests from customers.
Events: ExtensionReceived, AcceptExtension, RejectExtension, ExtensionTimeout
States: ExtensionIdle, ExtensionPending(duration, price), ExtensionAccepted, ExtensionRejected
4.3 Screen Changes
| Screen | Changes |
|---|---|
| Home screen | Active sessions list → tap to open chat |
| New: Chat screen | Full chat UI (same as client app) |
| Chat screen | Extension request overlay when customer requests extension |
| New: Extension dialog | "Customer ingin perpanjang X menit" with Accept/Reject buttons |
| New: Goodbye screen | Free-text input for closing message |
| New: Chat history list | List of past sessions (customer name, date, goodbye message) |
| New: Chat transcript screen | Read-only scrollable chat history |
4.4 Navigation Updates
New routes in GoRouter:
/chat/session/:sessionId— active chat screen/chat/history— chat history list/chat/history/:sessionId— read-only transcript
4.5 FCM Setup
- Same as client app: request permission on launch, register token
- Push notification on incoming message when app is backgrounded
- Tap notification → navigate directly to specific customer's chat screen (not just chat list)
5. Control Center Changes
5.1 Updated Pages
| Page | Changes |
|---|---|
| Settings page | Add: free trial toggle + duration, extension timeout, early end toggles (mitra/customer) |
| Session detail page | Add: view chat transcript link (role-restricted, deferred) |
5.2 No New Pages This Phase
Chat transcript viewing for admins is deferred. Config controls are added to the existing Settings page.
6. Implementation Order
| Step | What | Apps affected |
|---|---|---|
| 1 | Database migration (new tables, altered columns, new config keys) | Backend |
| 2 | WebSocket plugin setup (@fastify/websocket) |
Backend |
| 3 | Pricing service + free trial eligibility + chat opening API | Backend |
| 4 | Chat message service + WebSocket message handling | Backend |
| 5 | Message delivery/read status tracking | Backend |
| 6 | Session timer service (backend-authoritative countdown) | Backend |
| 7 | Extension service (request/response/timeout) | Backend |
| 8 | Closure service (goodbye messages, session completion) | Backend |
| 9 | FCM notification service + device token endpoint | Backend |
| 10 | Client app: FCM setup + pricing dialog + ChatOpeningBloc | Client app |
| 11 | Client app: ChatBloc + chat screen + message status UI | Client app |
| 12 | Client app: timer warning + extension dialog + SessionClosureBloc | Client app |
| 13 | Client app: goodbye screen + chat history screens | Client app |
| 14 | Mitra app: FCM setup + ChatBloc + chat screen | Mitra app |
| 15 | Mitra app: ExtensionBloc + extension dialog | Mitra app |
| 16 | Mitra app: goodbye screen + chat history screens | Mitra app |
| 17 | Control center: settings page updates (free trial, extension, early end) | Control center |
| 18 | Typing indicator (WebSocket relay, throttle, 3s timeout) | Backend + both apps |
7. New Dependencies
| App | Package | Purpose |
|---|---|---|
| Backend | @fastify/websocket |
WebSocket support (built on ws) |
| Backend | firebase-admin (existing) |
FCM push notifications |
| Client app | web_socket_channel |
WebSocket client |
| Client app | firebase_messaging |
FCM push notifications |
| Mitra app | web_socket_channel |
WebSocket client |
| Mitra app | firebase_messaging |
FCM push notifications |
8. Notes for Future Phases
Media messages (image, voice clip, video clip):
- Add file upload endpoint (Cloud Storage)
- Add new
typevalues tochat_messages - Add media bubble widgets in Flutter
- No architectural changes — same WebSocket transport, same delivery status, same history
Chat transcript for Control Center:
- Add role-based access check
- Reuse existing
/api/shared/chat/:sessionId/transcriptendpoint on internal routes
Deletion requests:
- Add customer request flow + admin approval
- Soft-delete or anonymize messages in
chat_messages
Payment integration (Xendit):
- Replace mock pricing with real payment flow
- Integrate at the
pending_paymentstatus transition point - Extension payments follow same flow