Design Doc: Merchant Support Form
Feature Name
Merchant Support Form — PWA form that creates Chatwoot conversations via NestJS
Overview
Add a support form inside the FlowPOS PWA (apps/frontend-pwa) that allows authenticated
merchants to report problems or submit feature requests. The form submits to a new NestJS
SupportModule which calls the Chatwoot REST API to create a contact and open a new
conversation in the support inbox — enriched with merchant context (businessId, plan, role).
The conversation appears immediately in the Chatwoot inbox for the support team to handle.
This feature depends on Chatwoot being deployed and configured (see
design-chatwoot-support-layer.md). It does not require the live chat widget to be active.
Project Context
- Project: FlowPOS
- Frontend:
apps/frontend-pwa(Vite 6, React 19, TypeScript, Tailwind CSS, i18next, React Hook Form + Zod, Shadcn/ui) - Backend:
apps/backend(NestJS 11, Hexagonal Architecture, Kysely, PostgreSQL) - Auth: Firebase (authenticated user available in all PWA routes)
- Multi-tenant: all operations scoped by
businessId - Chatwoot instance:
https://support.flowpos.app(self-hosted, same GCP project) - Secrets: Doppler (
CHATWOOT_API_TOKEN,CHATWOOT_ACCOUNT_ID,CHATWOOT_INBOX_ID) - No changes to FlowPOS PostgreSQL schema (no local persistence of support requests)
Scope
What this spec must cover
-
PWA Support Form UI (
apps/frontend-pwa):- New page/modal accessible from the main navigation or settings menu
- Form fields:
- Type (select): Bug Report / Feature Request / Billing Question / Other
- Subject (text input, required, max 100 chars)
- Description (textarea, required, min 20 chars, max 2000 chars)
- Attachments (file upload, optional, max 3 files, 5MB each — images and PDFs only)
- Form validation via Zod schema
- Loading, success, and error states
- On success: show confirmation message with a reference conversation ID
- i18n: all labels and messages in Spanish (
es) and English (en) - Component:
apps/frontend-pwa/src/pages/support/SupportFormPage.tsx
-
NestJS SupportModule (
apps/backend/src/support/):- Follows hexagonal architecture (domain / application / infrastructure / interfaces)
POST /support/conversations— authenticated endpoint (requires Firebase token)- Controller receives:
type,subject,description,attachments[](optional) - Service enriches the payload with merchant context from the authenticated session:
businessId,businessName,userEmail,userName,userRole,planTier
- Infrastructure adapter (
ChatwootAdapter) calls the Chatwoot REST API:- Upsert contact:
POST /api/v1/accounts/{id}/contacts/searchthenPOST /api/v1/accounts/{id}/contactsif not found - Create conversation:
POST /api/v1/accounts/{id}/conversationswith:inbox_idfrom env varcontact_idfrom step 1- Conversation attributes:
businessId,businessName,userRole,planTier,type - Labels: mapped from
typefield (e.g.,bug-report,feature-request)
- Send first message:
POST /api/v1/accounts/{id}/conversations/{id}/messageswith the subject + description as the message body - Upload attachments if present (multipart to Chatwoot messages API)
- Upsert contact:
- Returns:
{ conversationId: number }to the PWA ChatwootPortinterface defined in domain layer (swappable adapter)
-
Environment variable contract (Doppler, backend only):
CHATWOOT_API_TOKEN=<user_access_token_from_chatwoot_profile>
CHATWOOT_ACCOUNT_ID=<numeric_account_id>
CHATWOOT_INBOX_ID=<numeric_inbox_id_for_support_form>
CHATWOOT_BASE_URL=https://support.flowpos.app -
Chatwoot label setup (manual, documented setup step — not automated):
- Create labels in Chatwoot:
bug-report,feature-request,billing,other - These are applied automatically by the NestJS adapter based on form
type
- Create labels in Chatwoot:
Out of scope
- Live chat widget (covered in
design-chatwoot-support-layer.md) - Merchant-facing conversation history / ticket tracking in the PWA
- Email notifications to the merchant on agent reply (handled natively by Chatwoot)
- Admin UI for managing support requests inside FlowPOS
- Rate limiting on the support endpoint (future hardening)
- Unauthenticated / public support form
Key Technical Decisions
| Decision | Choice | Reason |
|---|---|---|
| API call location | NestJS backend (not PWA direct) | Keeps Chatwoot API token server-side; enables merchant context enrichment |
| Chatwoot integration layer | ChatwootAdapter behind ChatwootPort | Hexagonal pattern — swappable if tool changes |
| Contact upsert strategy | Search by email, create if not found | Avoids duplicate contacts per merchant |
| Conversation label mapping | Derived from form type field | Allows Chatwoot inbox routing rules per type |
| Attachment handling | Forwarded to Chatwoot messages API | Avoids storing files in FlowPOS infra |
| Local persistence | None | Chatwoot is the source of truth for conversations |
| Auth | Firebase token (existing AuthGuard) | Consistent with all other PWA→backend calls |
Speckit Command
Primary:
/speckit.specify Add a merchant support form to the FlowPOS PWA (apps/frontend-pwa) with fields for type (bug/feature/billing/other), subject, description, and optional attachments — the form submits to a new NestJS SupportModule (apps/backend) that enriches the request with authenticated merchant context (businessId, businessName, userRole, planTier) and calls the Chatwoot REST API via a ChatwootAdapter to upsert a contact, create a labeled conversation, and post the first message, returning a conversationId to the PWA; no local DB persistence; Spanish-first i18n; no changes to existing FlowPOS schema.
Alternative:
/speckit.specify Create a SupportModule in apps/backend with a POST /support/conversations endpoint that accepts type, subject, description, and attachments from an authenticated PWA user, enriches the payload with merchant context from the Firebase session, and calls Chatwoot REST API (contact upsert + conversation create + message post + attachment upload) via a hexagonal ChatwootAdapter behind a ChatwootPort; add a SupportFormPage.tsx in apps/frontend-pwa with Zod-validated form, i18n labels, and success/error states showing the returned conversationId.
Why the primary is best: The primary captures all three layers (PWA form, NestJS enrichment, Chatwoot API calls) in one sentence while calling out the two most important constraints: merchant context enrichment and no local DB persistence. It also explicitly names the conversation label strategy which drives inbox routing in Chatwoot. The alternative is more implementation-specific and useful as a fallback if Speckit needs explicit file paths or layer names to avoid generating unnecessary schema migrations.