Skip to main content

Ecommerce Adapter Architecture

Source-backed reference for apps/backend/src/ecommerce/.

For incident triage and operational recovery, see Shopify Ecommerce Troubleshooting Runbook.

Current provider status in code:

  • Shopify: implemented
  • WooCommerce: not implemented in the backend yet. The database enum contains the provider value, but there is no WooCommerce adapter under apps/backend/src/ecommerce/, the connect DTO only accepts shopify, and the provider factory does not register WooCommerce.

Scope

The ecommerce module handles:

  • OAuth connection lifecycle
  • outbound sync (products, inventory, collections)
  • inbound order ingestion (webhooks + polling fallback)
  • sync logging and connection diagnostics

Key files:

  • application/ecommerce.service.ts
  • domain/ecommerce-provider.port.ts
  • infrastructure/ecommerce-provider.factory.ts
  • infrastructure/shopify/*
  • interfaces/ecommerce.controller.ts

Hexagonal mapping

Domain layer

  • ecommerce-provider.port.ts: provider contract
  • ecommerce-connection.domain.ts: repository contracts and records

Domain expresses what the module needs from a provider without HTTP/SDK specifics.

Application layer

  • EcommerceService: orchestration for connect/sync/poll/webhook ingestion/logging
  • listeners under application/listeners/: event-driven sync triggers

Infrastructure layer

  • EcommerceConnectionRepository (Kysely persistence)
  • EcommerceProviderFactory (provider-name -> adapter mapping)
  • EcommercePollingProcessor (BullMQ queue worker)
  • ShopifyAdapter + ShopifyWebhookController

Interfaces layer

  • EcommerceController for authenticated operational endpoints
  • public Shopify webhook and OAuth callback endpoints
  • DTO/query parsing

Provider port and extension pattern

EcommerceProviderPort is the stable contract for all adapters:

  • OAuth URL and code exchange
  • webhook registration and signature verification
  • product and collection push
  • inventory level update
  • order pull and acknowledge
  • webhook payload parsing

EcommerceProviderFactory currently maps:

  • shopify -> ShopifyAdapter

To add WooCommerce, implement a WooCommerce adapter that satisfies EcommerceProviderPort, expose provider-specific credentials through the interface layer, and register it in the factory. Application flow should remain unchanged once the adapter satisfies the same port.

Adapter implementation checklist:

  1. Keep platform API calls, HMAC validation, and payload parsing inside the infrastructure adapter.
  2. Return only the domain DTOs from ecommerce-provider.port.ts to the application layer.
  3. Register the adapter in EcommerceProviderFactory and module providers.
  4. Verify OAuth, webhook registration, product push, inventory push, polling, and acknowledgement behavior with provider-specific tests.
  5. Keep EcommerceService orchestration provider-neutral; if a behavior is only needed by one platform, prefer an adapter implementation detail over a new domain concept.

Until a WooCommerce adapter is registered in the factory and accepted by the DTOs, API consumers should treat woocommerce as reserved forward compatibility rather than an available provider.


Runtime flows

1) OAuth connection

  1. GET /ecommerce/oauth/url returns provider authorization URL (state includes business + provider context).
  2. Provider redirects to GET /ecommerce/oauth/callback.
  3. Service exchanges code for token, attempts webhook registration, encrypts credentials, stores connection.
  4. Polling repeat job is registered.
  5. Backend redirects to PWA settings route.

2) Outbound sync

Event-driven:

  • product sync listener -> syncProduct
  • inventory sync listener -> syncInventoryLevel
  • collection sync listener -> syncCollection

Manual:

  • POST /ecommerce/sync/products
  • POST /ecommerce/sync/collections

Batch pushes run paginated loops and emit summary sync logs.

3) Inbound orders

Webhook path:

  • POST /ecommerce/webhooks/shopify
  • requires raw request body for HMAC verification (express.raw route behavior)
  • validates HMAC using stored webhook secret
  • parses payload via adapter
  • ingests deduplicated order

Polling fallback:

  • BullMQ queue ecommerce-polling, job poll
  • pulls orders since lastSyncedAt ?? createdAt
  • ingests + acknowledges orders
  • updates lastSyncedAt

Current ingestion scope:

  • creates an ecommerce_ingested_order row for deduplication
  • creates or links a customer when the external order includes an email
  • deducts mapped product inventory
  • writes a sync log

The schema has a saleId field and repository method for linking an ingested order to a FlowPOS sale, but the current service does not create a sale record or call updateWithSale. Treat sale creation from Shopify orders as a future extension, not implemented behavior.


Persistence responsibilities

EcommerceConnectionRepository handles:

  • connection create/read/update/delete
  • product mapping upsert and lookup
  • collection mapping upsert and lookup
  • ingested-order deduplication guard
  • sync log persistence and pagination
  • acknowledgement timestamps and connection status/sync timestamps

Credential data is encrypted at rest through CryptoService.


Queue and polling behavior

Polling queue:

  • queue name: ecommerce-polling
  • repeat job: poll
  • repeat interval: every 15 minutes
  • repeat job id pattern: ecommerce-poll-{businessId}

Worker:

  • processor: EcommercePollingProcessor
  • startup dedup uses repeatable job.key (not job.id) to avoid accidental deletion of valid jobs
  • failures are rethrown for BullMQ retry handling

Manual poll:

  • POST /ecommerce/sync/poll enqueues one immediate poll job
  • GET /ecommerce/debug/poll runs poll synchronously for debug workflows

API surface (high-level)

Authenticated:

  • GET /ecommerce/connection
  • GET /ecommerce/oauth/url
  • GET /ecommerce/sync/log
  • POST /ecommerce/sync/products
  • POST /ecommerce/sync/collections
  • POST /ecommerce/sync/poll
  • GET /ecommerce/debug/poll
  • GET /ecommerce/debug/scopes
  • DELETE /ecommerce/connection

Public:

  • GET /ecommerce/oauth/callback
  • POST /ecommerce/webhooks/shopify

Most authenticated endpoints are scoped by businessId query param. The current controller uses that value directly; it does not apply a CASL ability decorator or compare the query value to the Firebase user's active business claim.


Operational pitfalls

  • Webhook registration can fail during connect; polling fallback is still used.
  • Webhook controller intentionally returns 200 { ok: false } for app-side errors (non-auth) to prevent provider retry storms.
  • Polling jobs are deduplicated by stable repeat jobId (ecommerce-poll-{businessId}).
  • disconnect is idempotent: returns success even when no connection exists.
  • Scope debugging endpoint (/ecommerce/debug/scopes) is intended for operational troubleshooting, not merchant workflows.
  • Provider factory currently maps only shopify; unsupported provider names fail fast with Unknown e-commerce provider.
  • API consumers should treat WooCommerce as unavailable until the adapter is implemented and registered, even though the provider enum exists for forward compatibility.
  • OAuth state is base64url-encoded JSON with business/provider/shop context; it is not a signed or TTL-bound token in current code.
  • Shopify inventory_levels/update is registered as a webhook topic, but the adapter currently parses only orders/create; inbound inventory updates from Shopify are ignored.
  • Outbound variant inventory sync uses the FlowPOS product-level quantity when pushing to Shopify inventory levels. Do not document variant-specific stock as synchronized until the service reads variant inventory quantities.
  • Shopify inventory updates use the mapped Shopify inventory_item_id stored in externalVariantId.