Ecommerce Adapter Architecture
Source-backed reference for apps/backend/src/ecommerce/.
Current provider status in code:
- Shopify: implemented
- WooCommerce: not yet implemented (port and factory pattern are ready)
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.tsdomain/ecommerce-provider.port.tsinfrastructure/ecommerce-provider.factory.tsinfrastructure/shopify/*interfaces/ecommerce.controller.ts
Hexagonal mapping
Domain layer
ecommerce-provider.port.ts: provider contractecommerce-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
EcommerceControllerfor 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 and register it in the factory. Application flow remains unchanged.
Runtime flows
1) OAuth connection
GET /ecommerce/oauth/urlreturns provider authorization URL (stateincludes business + provider context).- Provider redirects to
GET /ecommerce/oauth/callback. - Service exchanges code for token, attempts webhook registration, encrypts credentials, stores connection.
- Polling repeat job is registered.
- 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/productsPOST /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.rawroute behavior) - validates HMAC using stored webhook secret
- parses payload via adapter
- ingests deduplicated order
Polling fallback:
- BullMQ queue
ecommerce-polling, jobpoll - pulls orders since
lastSyncedAt ?? createdAt - ingests + acknowledges orders
- updates
lastSyncedAt
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(notjob.id) to avoid accidental deletion of valid jobs - failures are rethrown for BullMQ retry handling
Manual poll:
POST /ecommerce/sync/pollenqueues one immediate poll jobGET /ecommerce/debug/pollruns poll synchronously for debug workflows
API surface (high-level)
Authenticated:
GET /ecommerce/connectionGET /ecommerce/oauth/urlGET /ecommerce/sync/logPOST /ecommerce/sync/productsPOST /ecommerce/sync/collectionsPOST /ecommerce/sync/pollGET /ecommerce/debug/pollGET /ecommerce/debug/scopesDELETE /ecommerce/connection
Public:
GET /ecommerce/oauth/callbackPOST /ecommerce/webhooks/shopify
Most authenticated endpoints are scoped by businessId query param.
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}). disconnectis 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 withUnknown e-commerce provider.