Skip to main content

RFC Summary: Embeddable Deal & Ticket Widgets — Index + Deal Create + Ticket Create

Full RFC: Embeddable Deal & Ticket Widgets — Index + Deal Create + Ticket Create (see scratch/rfc/embed-deal-ticket-index-deal-create-ticket-create-rfc.md) Status: IDEA | Type: full-stack / enhancement Author: Engineering (RFC author) | Target: 2026-Q3 PRD: Create Deals & Tickets While Viewing Chat


Problem

Omnichannel agents handling live conversations cannot see existing deals or tickets associated with the room, or create new ones, without leaving the conversation context. The current full-screen bottom sheet overlays hide the chat during form entry, forcing agents to repeatedly close and reopen forms to reference conversation details. Unsaved form data is silently discarded on accidental close, causing data loss and discouraging record creation.


What's Being Built

  • Deal index embed page (/embed/deals/room/:room_id) — shows the most recent deal for a room, or empty state with create button
  • Deal create embed page (/embed/deals/new) — new wrapper component reusing existing DealsCreate* form components, with proper error toast, cancel confirm, and typed postMessage
  • Ticket index embed page (/embed/tickets/room/:room_id) — shows the most recent ticket for a room, or empty state with create button
  • Ticket create embed page (/embed/tickets/create) — already exists, security hardening only (targetOrigin restriction)
  • Infrastructure: Verify frame-ancestors in SPA Helm config (values-production.yaml) covers parent origin; remove X-Frame-Options: SAMEORIGIN from nginx if parent is cross-origin
  • Security: Typed postMessage contract, restricted targetOrigin (never '*'), unsaved changes protection, error toast on create failure

Component Architecture

Component Architecture
flowchart LR
subgraph Browser
A[Parent Omnichannel App]
B[crm-fe-v3 iframe SPA]
end
C[CDN]
D[IAG Internal API Gateway]
subgraph qontak_com [qontak.com Rails 5.2]
E[V2dot8 DealsController]
F[V2dot8 TicketsController]
end
G[(Postgres)]

A -->|opens iframe with token and parent_origin| B
B --> C
B -->|API calls via IAG| D
D --> E
D --> F
E --> G
F --> G
B -->|postMessage| A

Repo Map (files touched)

qontak.com (Rails)
└── db/data/20260701000001_add_feature_embed_deal_sanitize.rb — NEW: seed migration

crm-fe-v3 (Nuxt 4) — Infrastructure:
├── deploy/nginx/default.conf:28 — MODIFIED: remove X-Frame-Options SAMEORIGIN
└── deploy-alicloud/chart/values-production.yaml — VERIFY: frame-ancestors allowlist

crm-fe-v3 (Nuxt 4) — Pages:
├── layers/embed-deal/
│ ├── pages/embed/deals/room/[roomId].vue — NEW: deal index page
│ ├── components/EmbedDealIndexPage.vue — NEW: deal index component
│ ├── pages/embed/deals/new/index.vue — NEW: deal create page
│ └── components/EmbedDealNewPage.vue — NEW: deal create wrapper
├── layers/embed-ticket/
│ ├── pages/embed/tickets/room/[roomId].vue — NEW: ticket index page
│ └── components/EmbedTicketIndexPage.vue — NEW: ticket index component
├── layers/embed-deal/composables/useEmbedDealCreate.ts — MODIFIED: 6 gap fixes
├── layers/embed-deal/composables/useEmbedDealAuth.ts — MODIFIED: authError on 401
└── layers/embed-ticket/composables/useEmbedTicketCreate.ts — MODIFIED: targetOrigin

Deal Form State Machine

Deal Form State Machine
stateDiagram-v2
[*] --> Loading: Page opens
Loading --> Form: Auth OK
Loading --> AuthError: No / invalid token
Loading --> LoadError: API failure

Form --> Dirty: User edits any field
Dirty --> Form: All fields reverted

Dirty --> Submitting: Save
Form --> Submitting: Save

Submitting --> Success: API 201
Submitting --> Dirty: API error (validation / network)

Success --> [*]: postMessage + close
AuthError --> [*]: Show error
LoadError --> [*]: Show error

Dirty --> [*]: Cancel with confirm dialog
Form --> [*]: Cancel closes immediately

Sequence Diagrams

Happy Path — Deal Index

Happy Path — Deal Index
sequenceDiagram
participant Parent as Parent App
participant SPA as SPA
participant IAG as IAG
participant API as Rails API
participant DB as Postgres

Parent->>SPA: Open iframe /embed/deals/room/123
Note over SPA: token + parent_origin + can_create from query params
SPA->>SPA: useEmbedDealAuth reads token
SPA->>IAG: GET /api/internal/v1/deals?room_id=123&per_page=1
IAG->>API: Forward to v2.7 crm/deals#index
Note over API: AdvancedSearch merges room_id filter
API->>DB: Query crm_deals by team_id and room_id LIMIT 1
DB-->>API: latest deal or empty
API-->>IAG: 200 with deals array and pagination
IAG-->>SPA: Response
SPA->>SPA: Render deal card or empty state

Failure Path — Auth Error

Failure Path — Auth Error
sequenceDiagram
participant Parent as Parent App
participant SPA as SPA

Parent->>SPA: Open iframe without token param
SPA->>SPA: useEmbedDealAuth finds no token, isAuthenticated = false
Note over SPA: authError also set on 401 from API (hardened)
SPA->>SPA: Render EmbedDealAuthError component

Key Technical Decisions

#DecisionChosenRationale
1Ticket create pageHarden only, don't rebuildAlready exists and fully functional. Only targetOrigin needs fixing.
2Deal index data sourceExisting v2.7 index + ?room_id=Filter already in Crm::AdvancedSearch (advanced_search.rb:336). No backend change.
3Ticket index data sourceExisting v2.8 index + ?channel_integration_room_id=Same pattern as deal index. Filter in advanced_search.rb:1457.
4Deal create pageNew wrapper at /embed/deals/new, old as fallbackOld 480-line component has gap fixes needed. New wrapper uses stable DealsCreate* components.
5postMessage contractTyped messages with type fieldParent app needs to discriminate message types reliably. Ticket already typed.
6Clickjacking defenseUpdate SPA nginx X-Frame-Options + verify Helm frame-ancestorsSAMEORIGIN blocks cross-origin iframe. frame-ancestors already set at K8s ingress level.

APIs at a Glance

MethodEndpointStatusOwner
GET/api/internal/v1/dealsExtended (optional ?room_id= filter)BE — no code change
GET/api/internal/v1/ticketsExtended (optional ?channel_integration_room_id= filter)BE — no code change
POST/api/internal/v1/dealsExists with embed supportBE — reused
POST/api/internal/v1/ticketsExists with embed supportBE — reused

No new endpoints. Both index filters already exist in Crm::AdvancedSearch.


Open Blockers

#BlockerOwnerBlocks
1None — all 4 OQs resolved

RFC is ready for execution. Remaining items: verify SPA frame-ancestors allowlist, remove nginx X-Frame-Options SAMEORIGIN if needed, coordinate parent app postMessage.


Rollout

StageAudienceGo/No-go evidence
Verify SPA frame-ancestors allowlist + nginx XFOProductionSPA page headers include frame-ancestors; nginx X-Frame-Options removed if cross-origin
Seed migrationProductionFeature flag exists in DB
Index pages (deal + ticket)ProductionDeal/ticket cards load by room via /api/internal/v1/ endpoints
Deal create new pageProductionTyped deal-created postMessage received by parent app
Security hardeningProductionNo '*' targetOrigin in any postMessage call; error toast on create failure

Stop conditions: frame-ancestors blocks parent app → update SPA Helm allowlist. X-Frame-Options SAMEORIGIN blocks cross-origin → remove from nginx. PostMessage contract breaks parent → revert to old format.

Rollback: Deploy: SPA infra → seed → FE. Rollback: FE → seed → SPA infra. Index filters are optional params — backward compatible.