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 acceptsshopify, 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.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, 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:
- Keep platform API calls, HMAC validation, and payload parsing inside the infrastructure adapter.
- Return only the domain DTOs from
ecommerce-provider.port.tsto the application layer. - Register the adapter in
EcommerceProviderFactoryand module providers. - Verify OAuth, webhook registration, product push, inventory push, polling, and acknowledgement behavior with provider-specific tests.
- Keep
EcommerceServiceorchestration 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
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
Current ingestion scope:
- creates an
ecommerce_ingested_orderrow 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(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. 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}). 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. - API consumers should treat WooCommerce as unavailable until the adapter is implemented and registered, even though the provider enum exists for forward compatibility.
- OAuth
stateis base64url-encoded JSON with business/provider/shop context; it is not a signed or TTL-bound token in current code. - Shopify
inventory_levels/updateis registered as a webhook topic, but the adapter currently parses onlyorders/create; inbound inventory updates from Shopify are ignored. - Outbound variant inventory sync uses the FlowPOS product-level
quantitywhen 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_idstored inexternalVariantId.