App overview & sales-channel role
FKC-OmniChannel unifies inbound customer conversations from WhatsApp, Instagram, Facebook Messenger, web chat, email and SMS into one merchant inbox, layered with AI-assisted replies and automated workflows. Inside that inbox, agents need to recommend, link to and sell the merchant's Shopify products without leaving the ticket — which is why the app is registered as a Shopify sales channel, not just an admin extension.
On install, the app provisions a modern Publication via channelCreate (Channel API 2026-04). Merchants then see “FKC-Omnichannel” in the standard product publication picker alongside Online Store and POS, and choose which products are eligible to be surfaced inside customer chats. When an agent or the AI shares a product, the app generates a Storefront cart permalink so any resulting order is attributed back to the FKC publication.
System topology
Two deployed surfaces, deliberately split so that webhook delivery, OAuth, message processing and storage all live on Cloudflare while the embedded admin UI lives on a Node host.
┌───────────────────────────────────────────────┐
│ Shopify Admin (embedded iframe, App Bridge) │
│ shop.fkc-omnichannel.com — React Router │
└──────────────┬────────────────────────────────┘
│ App Bridge / authenticated session
▼
┌───────────────────────────────────────────────┐
│ apps/web — Cloudflare Worker (Next.js) │
│ • OAuth start + callback │
│ • /api/webhooks/shopify (HMAC, topic-routed)│
│ • /api/webhooks/meta (WhatsApp / IG / FB)│
│ • Inbox UI, AI agent, workflow engine │
│ Storage: │
│ D1 — workspaces, conversations, audit log │
│ R2 — media attachments (images, voice) │
│ Vectorize — KB embeddings (per workspace) │
│ KV — sessions, rate-limits, dedupe keys │
└───────────────────────────────────────────────┘
▲ ▲ ▲
Meta Graph API WhatsApp Cloud OpenAI
(IG, Messenger) API (WABA) (per-workspace key)End-to-end data routing
Every inbound message follows the same five-stage pipeline regardless of the originating channel:
1. INGEST POST /api/webhooks/<channel> ← HMAC / signature verified
↓ idempotency key written to KV
2. NORMALISE Channel adapter → canonical Message DTO
3. RESOLVE Match/create Contact + open/attach Conversation
4. PERSIST Prisma write to D1 · attachments to R2
↓ Durable Object fan-out to live inbox
5. AUTOMATE Workflow engine evaluates triggers
AI Sales Agent drafts a suggested reply
↓ Agent sends → outbound adapter (Meta / WABA / SMTP)The same path applies in reverse: an agent reply is written to D1 first, then dispatched to the originating channel, and any platform-specific message ID is stored back on the row so delivery and read receipts can be correlated.
Ticket / conversation lifecycle
The unit of work in the inbox is a Conversation (a.k.a. ticket) with a deterministic state machine:
open ──assign──► assigned ──reply──► waiting_customer
▲ │ │
│ └─ customer replies ──┘
└────── reopen ──── resolved ◄───────────┘
│
└── auto-close after N days idleAI assistance never changes ticket state on its own — it only drafts a reply that an agent reviews. The send action is the state transition. Every state change writes an immutable audit entry with actor, before, after, and source (manual / workflow / webhook).
Shopify integration surface
- Channel config extension declares
type = "channel_config"so the channel appears in the publication picker immediately on install. - Modern Publication — the install callback calls
channelCreate(Channel API 2026-04) to provision the Publication, required for the channel to be selectable. - Embedded admin UI — React Router framework, App Bridge and Polaris, for in-admin connection management.
- Cart permalinks & attribution built using the Storefront Access Token for the FKC publication, so orders are attributed to this sales channel.
- Order & product webhooks land on
/api/webhooks/shopify, routed by thex-shopify-topicheader.
Webhooks & verification
POST /api/webhooks/shopify is a single unified handler for every subscribed topic:
1. Read raw body (bytes, not parsed — required for HMAC)
2. Verify X-Shopify-Hmac-Sha256 ← reject 401 on mismatch
3. Dedupe KV.put(X-Shopify-Webhook-Id) ← 24h no-op window
4. Route switch (X-Shopify-Topic) { ... }
app/uninstalled → revoke tokens
orders/create → attach to conversation
customers/redact → enqueue GDPR delete job
shop/redact → wipe shop + child rows
5. Persist Audit log entry for every privacy webhook
6. Respond 200 { ok: true } within ~5s budgetNon-Shopify channels use the same shape: /api/webhooks/meta verifies X-Hub-Signature-256 against the Meta app secret; HMAC mismatches return 401 and are logged.
OAuth scopes & why each is needed
read_products show catalog in the product picker read_orders display recent orders in the context panel read_customers match a conversation to a Shopify customer write_draft_orders create a draft order for a custom quote in chat read_inventory show "in stock / N left" beside products read_publications list publications when registering the channel write_publications publish/unpublish products to the FKC channel read_product_listings required by the channelCreate mutation read_content ground AI answers in the merchant's own policies read_price_rules surface active discounts the AI may mention
Security, secrets & data handling
- Transport. All public endpoints are HTTPS-only via Cloudflare (HSTS preload).
- Token storage. Every channel access token is encrypted at rest with AES-GCM using a per-deployment master key held in Worker Secrets, never written to D1 in plaintext.
- Sessions. NextAuth cookies — HttpOnly, Secure, SameSite=Lax; records in KV with short TTL.
- Webhook integrity. Every provider webhook is HMAC- or signature-verified on the raw body before any database access.
- Tenant isolation. Every query carries the workspace ID; row-level filters and a request-scoped context enforce it server-side.
- Audit log. Security-sensitive actions write immutable audit entries.
- No model training. Customer message content is never used to train third-party models; AI calls run with the merchant's own API key.
GDPR / mandatory privacy webhooks
- customers/data_request — enqueues an export job; we email the export to the shop owner within 30 days.
- customers/redact — deletes the matching contact and all associated messages, attachments and PII across D1 and R2.
- shop/redact — fired 48h after uninstall; cascades a full delete of the workspace integration row, cached products, conversations and the encrypted token blob.
Public user-facing data deletion: see Data Deletion.
Reviewer walkthrough & test path
- Install. From the App Store listing, install on a development store. OAuth completes and the channel record + modern Publication are provisioned automatically.
- Verify channel registration. In Admin → Products → Publishing, confirm “FKC-Omnichannel” is listed alongside Online Store and POS; publish at least one product to it.
- Trigger an inbound message. A new conversation appears in the inbox in real time via Durable Object fan-out, in the open state.
- Demonstrate routing. Assign, reply, and observe the transition to waiting_customer; audit entries appear under Activity.
- Share a product in chat using the in-conversation picker and send a cart permalink.
- Complete checkout.
orders/createfires, is HMAC-verified, attributed to the conversation, and appears inline. - Resolve the conversation; a follow-up inbound message reopens it automatically.
- Uninstall.
app/uninstalledwipes the token within seconds;shop/redact(T+48h) cascades the rest.