Skip to main content
A commerce provider is a store platform the platform can attribute sales on. Shopify is the main one. Others connect through the same interface, so that every provider shares one attribution engine, one escrow and ledger, and one on-chain record. The rule for every provider is the same: normalise your world into neutral inputs and delegate. A provider never touches campaigns, escrow, the ledger or the chain directly.

The provider contract

Each provider is one registry entry, defined in src/modules/commerce/registry/commerce-provider.ts:
  • The registry rejects a declared capability without its port, and a port without its capability. Declare only what you implement.
  • Every store row stores its provider key (the provider column on the store table). The registry routes each store-scoped operation by that key at runtime.
  • A feature that needs a capability checks for it at its boundary. A store can be connected even if its provider lacks some capabilities.

Capabilities

Providers today

Two internal providers exist only to exercise the abstraction in development and automated tests.

Shopify

Shopify is the reference implementation (src/modules/commerce-providers/shopify/ plus the older src/modules/shopify/).
  • Connection. A brand installs the app and authorises the store with OAuth. The access token is stored per store connection. Stores are soft-deleted only; uninstalling never removes history.
  • Webhooks. Orders created, refunds created, disputes created and updated, orders fulfilled, checkouts updated, products updated and deleted, app uninstalled, and the mandatory privacy compliance topics. Every webhook is verified by HMAC before it is read.
  • Attribution. Shopify orders use the dedicated ladder described in Attribution engine, fed by the theme embed’s cart and line-item carriers, the Admin API and the customer journey.
  • Billing. Brands subscribe through Shopify’s app pricing. When billing enforcement is switched on (off by default), creating and funding campaigns, among other actions, requires an active plan, or an active FREEQ stake as an alternative.
  • Extra signals. Shopify’s order risk assessments and theme-embed health are consumed by the Shopify module directly; the matching capabilities in the interface are there for future providers.

Partner marketplace (partner wire)

A partner marketplace is integrated as a partner provider. It shows the pattern for a platform whose truth lives in a remote system:
  • Order events are pushed to POST /partner/stores/{storeId}/events, signed with HMAC-SHA256 over the raw body (X-Uam-Hmac-Sha256), with X-Uam-Event-Id and X-Uam-Event-Type headers that must match the body. Delivery is at-least-once from a transactional outbox; the inbox de-duplicates on eventId. 2xx means accepted (duplicates included), 4xx is terminal, 5xx should be retried.
  • Creator provisioning is a synchronous, idempotent call (POST /partner/stores/{storeId}/publishers) keyed on the partner’s own user id. It returns the creator’s share link.
  • Earnings are pulled per creator with a bearer key that is separate from the signing secret, so a leaked read key cannot forge events. The payout counter is cumulative and never decreases; the partner credits the difference since its last checkpoint.
  • Eligibility comes from the partner: each order carries an authoritative first-time-buyer flag and a buyer id used for the self-referral check.
The frozen wire is versioned separately and shared with the partner directly. Store-level reporting for partners is the Partner analytics API.

Partner tenants are not providers

A partner that brings its own users and frontend (for example, a partner app signing users in with its own Clerk instance) is a tenant, not a provider. Its brands still connect a Shopify store; its users call the application API directly from the browser. See Architecture: authentication and tenants.

How a new provider plugs in

  1. Pick a provider key and implement the ports for the capabilities you can honestly support. External product and order ids are opaque strings; never coerce them to numbers.
  2. Register stores through StoreConnectionService.registerProviderStore with your key. Membership, invitations, entitlement and the store switcher work unchanged.
  3. Ingest orders. Your inbound adapter (webhook receiver, poller or checkout endpoint) verifies and normalises payloads into the neutral CommerceOrderEvent union (ORDER_PLACED, ORDER_REFUNDED, ORDER_FULFILLED, DISPUTE_CHANGED) and submits them to OrderIngestionService. The inbox gives you de-duplication, claim and backoff retries, dead-lettering and replay.
  4. Process events in your OrderEventProcessor. Return processed, retryable or terminal, derived from persisted state, never from “the promise resolved”. The reason is a stable, low-cardinality slug with no raw provider text and no personal data.
  5. Delegate the money work to the shared OrderLifecycleEngine:
unitPriceMinor (net of discounts, excluding tax) on each line is optional. Commission is always a percentage of the order total, so line prices do not change the payout. If an order used a platform discount code, persist it on the order row at ingest; the neutral input deliberately carries no codes. 6. Add the provider to commerce-providers/commerce-provider-composition.ts, behind a flag until it is proven.

Rules beyond the interface

  • Idempotency. Duplicate deliveries must end processed, not double-applied. Escrow and ledger writes are idempotency-keyed by store, campaign and order.
  • No personal data in the inbox envelope. It is stored and replayed.
  • Fail closed. A click or code minted for one store is rejected at another store’s checkout. Unresolvable attribution ends UNATTRIBUTED; it is never guessed.
  • Opaque ids everywhere.
The test-shop provider under commerce-providers/fern/ is the smallest complete example (catalogue, discount codes, order events and engine delegation). Start there.