2 min read
Sales channels
Each channel’s state (not_connected, requested, first_sync, connected, syncing,
error, expired, disconnected, paused) lives on store.preferences.salesChannels
(@commerce/core/sales-channels). The live connection to a platform lives in two tables
(migration 0131):
channel_connection: one row per store and provider. Tokens are encrypted withSETTINGS_SECRET.channel_listing: one row per item the provider holds, with a hash, so a sync sends only changes and deletes what left.
| Channel | Provider | Platform API |
|---|---|---|
| Instagram, Facebook, WhatsApp | meta | Graph API v21: one business product catalog (/{catalog}/items_batch); WhatsApp links it to the WABA (/{waba}/product_catalogs) |
google | Merchant API v1: an API data source, productInputs:insert / delete | |
| ChatGPT | openai | The full feed POSTed to the endpoint + key OpenAI gives an approved merchant |
| Perplexity, Copilot, Google AI | none | still a request (requested) |
Code: packages/core/src/sales-channels/:
catalog.ts: items, one per sellable variant.providers.ts: plain-fetchclients,ChannelApiErrorwithauth/api.connect.ts: signed OAuth state, connect, pause, disconnect.sync.ts: the sync itself.
Connect flow
salesChannels.connect→startChannelConnect:- With the platform’s app configured, it returns a sign-in URL.
- If the store already holds that provider, the channel joins it (
connected). - Otherwise the old request.
- Facebook or Google redirects to
/api/channels/<meta|google>/callback. The route:- checks the HMAC-signed state (30 min, store + channel + nonce; Google adds PKCE derived from the nonce);
- checks the signed-in member belongs to that store;
- runs
completeChannelConnect: code → token (Meta: long-lived, ~60 days; Google: refresh token), find or create the catalog / data source, save encrypted, setfirst_sync; - queues a sync.
- ChatGPT: after OpenAI approves, the merchant pastes the feed endpoint and key
(
salesChannels.saveOpenAiFeed).
Sync
syncChannelProvider(db, storeId, provider, deps):
- A short read: the connection, live channels on that provider, the items, and the listing.
- Mark the channels
syncing(or keepfirst_sync). - Push outside any transaction: Meta upserts and deletes in batches of 1,000; Google 4 at a time, refreshing the token first when it is about to expire; OpenAI the whole feed.
- Success: upsert listing hashes, mark the channels
connected.- An
autherror marks themexpired. - Any other error marks them
errorwith the platform’s message.
- An
Worker queue {channels}-sync: a sweep every 6 hours queues one job per store × provider
(stores_with_channel_connections). The admin queues one after Connect, on Sync now and on
resume. AI agents get only what Agentic sales → Controls leaves visible; social channels get every
live product.
Platform setup (production)
| Env key | Where | Notes |
|---|---|---|
META_APP_ID, META_APP_SECRET | admin + worker | Meta app with Facebook Login for Business; catalog_management, business_management, whatsapp_business_management approved in App Review; redirect <ADMIN_URL>/api/channels/meta/callback |
GMC_GOOGLE_CLIENT_ID, GMC_GOOGLE_CLIENT_SECRET | admin + worker | Google OAuth web client with the Merchant API enabled and scope content verified; redirect <ADMIN_URL>/api/channels/google/callback. Blank: MERCHANT_GOOGLE_* is used |
Without these keys, Connect stays a request, so nothing breaks before App Review is done.
Not live-tested yet. The flows are covered by sync.test.ts against a fake platform. Run them
end to end once the Meta app, the Google verification and an OpenAI merchant approval exist.
Things to check then:
- the Merchant API v1 field names (
productAttributes, theIN_STOCKenums); - the OpenAI feed body.