Skip to content

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.

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.

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.

{
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_...",
}
}
  • externalId is the canonical store id (Shopify’s myshopify.com domain)
  • secretArn points to the Admin API access token in Secrets Manager
  • metadata.platform discriminates which backend (always "shopify" today)
  • metadata.storefrontAccessToken is the public-readable token the Next.js template uses for product/cart fetches
{
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 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.

You don’t create commerce rows directly — they’re created when the merchant completes the Shopify OAuth handshake:

  1. Operator drags the Commerce node onto the Blueprint
  2. Operator enters the merchant’s <shop>.myshopify.com subdomain
  3. Melbora signs a state token (HMAC) and redirects to Shopify’s OAuth screen
  4. Merchant approves the app’s scopes
  5. Shopify redirects back to /oauth/shopify/callback
  6. 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.

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_"));

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.

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.

When the merchant uninstalls the Shopify app:

  1. Shopify sends an APP_UNINSTALLED webhook
  2. Melbora’s webhook worker verifies the HMAC + cross-tenant header
  3. Removes the commerce row + all dependent channel_* rows
  4. Revokes the Admin API token in Secrets Manager

The Website itself stays — only the commerce-related services are removed.