Commerce + channels
import { Aside } from ‘@astrojs/starlight/components’;
Melbora models commerce as two layers: a commerce backend (today always Shopify) plus zero or more sales channels that publish the backend’s catalog to external surfaces.
The model
Section titled “The model”Website │ ├── service: commerce (Shopify Admin API + Storefront) │ └── metadata.platform: "shopify" │ ├── service: channel_tiktok (Shopify TikTok app) ├── service: channel_facebook (Shopify Meta app) ├── service: channel_pinterest (Shopify Pinterest app) ├── service: channel_google (Shopify Google & YouTube app) └── service: channel_marketplace_connect (Shopify Marketplace Connect)Each row is a separate WebsiteService. The commerce row holds the
store identity; channel rows hold per-channel install/publication state.
Why split commerce + channels?
Section titled “Why split commerce + channels?”Channel apps are separately installable Shopify apps. The merchant can have Shopify but not have TikTok installed; or have TikTok installed but not publishing the catalog yet. We want to model that distinction so the portal shows the truth — not “Shopify is connected, so we assume all channels are too.”
It also keeps the model platform-agnostic. The channel_* types don’t
mention Shopify; if a future BigCommerce adapter shipped with native
channel apps, they’d plug into the same channel rows.
The commerce row
Section titled “The commerce row”{ websiteId: "wb_01J7...", serviceType: "commerce", externalId: "joes-plumbing.myshopify.com", secretArn: "arn:aws:secretsmanager:...:secret:shopify-token-...", metadata: { platform: "shopify", currency: "USD", scopes: ["read_products", "write_products", ...], storefrontAccessToken: "shpat_...", }}externalIdis the canonical store id (Shopify’s myshopify.com domain)secretArnpoints to the Admin API access token in Secrets Managermetadata.platformdiscriminates which backend (always"shopify"today)metadata.storefrontAccessTokenis the public-readable token the Next.js template uses for product/cart fetches
Channel rows
Section titled “Channel rows”{ websiteId: "wb_01J7...", serviceType: "channel_tiktok", externalId: "tiktok", // the app handle in Shopify's app store metadata: { platform: "shopify", installed: true, publicationId: "gid://shopify/Publication/123", lastSyncedAt: "2026-05-25T10:00:00Z", }}Channel install status is detected by calling Shopify’s Publications API — we list the installed channel apps and emit a row per matching type. This runs automatically:
- When you mount the Blueprint canvas (so you see live status)
- After OAuth callback (so a freshly-connected channel shows up immediately)
- Periodically via cron (planned — today it’s mount + OAuth only)
Amazon (via Marketplace Connect)
Section titled “Amazon (via Marketplace Connect)”Amazon is not a separate backend — it rides on the
channel_marketplace_connect row. Shopify’s Marketplace Connect app
syncs the catalog out to Amazon and imports Amazon orders back into
Shopify, hands-off, for free. The merchant links their Amazon Seller
Central account once, inside Marketplace Connect (Melbora can’t
automate that link — Marketplace Connect exposes no public API for it),
and from then on it runs set-it-and-forget-it.
Because Amazon orders land back in Shopify, Melbora computes real
Amazon sales analytics from the Shopify Admin API we already hold a
token for — no Amazon Selling Partner API access: no Amazon
developer registration and no per-seller OAuth to stand up. The
Marketplace Connect row carries an amazon sub-blob:
{ serviceType: "channel_marketplace_connect", metadata: { platform: "shopify", channelKind: "marketplace_connect", isInstalled: true, amazon: { // "linked" the moment real Amazon orders are observed in Shopify; // otherwise "unknown" (we can't read MC's internal link state). linkState: "linked", // Deep-link into the merchant's Marketplace Connect Amazon setup. setupUrl: "https://acme.myshopify.com/admin/apps/marketplace-connect", analytics: { windowDays: 30, orderCount: 42, grossRevenue: 5123.5, currencyCode: "USD", averageOrderValue: 121.99, unitsSold: 58, lastOrderAt: "2026-05-26T18:02:00Z", daily: [/* per-day buckets */], topProducts: [{ title: "Linden Candle", units: 19 }], matchedSignals: ["channelName"], computedAt: "2026-05-27T09:00:00Z", }, analyticsRefreshedAt: "2026-05-27T09:00:00Z", }, }}Attribution (which Shopify orders are “Amazon”) keys primarily on the
order’s sales sub-channel name (channelDefinition.subChannelName,
where Marketplace Connect records the marketplace) or an amazon order
tag; the matched signal(s) are reported in analytics.matchedSignals so
an operator can confirm it against a real store. Analytics cover a
trailing window of up to 60 days (Shopify’s read_orders limit).
Read or recompute via:
const state = await vc.websites.amazonAnalytics("wb_01J7...");// → { shopifyConnected, marketplaceConnectPresent, linkState, setupUrl, analytics, ... }await vc.websites.refreshAmazonAnalytics("wb_01J7...", { window: 30 });Going further (Amazon account health, listing suppression, FBA inventory) requires Amazon’s SP-API — a future, opt-in upgrade that needs Amazon developer registration + per-seller OAuth. The scaffolding for it exists but is dormant; nothing calls Amazon today.
See Sell on Amazon for the walkthrough.
Connecting a commerce backend
Section titled “Connecting a commerce backend”You don’t create commerce rows directly — they’re created when the merchant completes the Shopify OAuth handshake:
- Operator drags the Commerce node onto the Blueprint
- Operator enters the merchant’s
<shop>.myshopify.comsubdomain - Melbora signs a state token (HMAC) and redirects to Shopify’s OAuth screen
- Merchant approves the app’s scopes
- Shopify redirects back to
/oauth/shopify/callback - Melbora exchanges the auth code for an access token, stores it in Secrets Manager, creates the commerce row, mints a Storefront access token
See Connect a Shopify store for the full walkthrough.
Reading commerce + channel state
Section titled “Reading commerce + channel state”const { services } = await vc.websites.services("wb_01J7...");const commerce = services.find(s => s.serviceType === "commerce");const channels = services.filter(s => s.serviceType.startsWith("channel_"));What the template renders
Section titled “What the template renders”The Next.js website-template reads products via Shopify’s Storefront
API using the public token from metadata.storefrontAccessToken. It
does not use the Admin API — the Admin token never leaves the
server side.
Product pages are statically generated with ISR (revalidate: 60), so
adding a product in Shopify shows up on the site within a minute
without a redeploy.
Checkout
Section titled “Checkout”Today checkout is handled by Shopify’s own checkout pages — the template’s BuyNow button redirects to a Shopify-hosted URL. Guest checkout is enabled. We’re constrained by Shopify’s ToS: custom checkout (Stripe-direct) is only allowed on Shopify Plus.
Channel teardown
Section titled “Channel teardown”When the merchant uninstalls the Shopify app:
- Shopify sends an
APP_UNINSTALLEDwebhook - Melbora’s webhook worker verifies the HMAC + cross-tenant header
- Removes the
commercerow + all dependentchannel_*rows - Revokes the Admin API token in Secrets Manager
The Website itself stays — only the commerce-related services are removed.
See also
Section titled “See also”- Connect a Shopify store — operator walkthrough
- Services — the underlying data model
- Webhooks — Shopify webhook contract