External systems are adapters behind application-owned ports. The default configuration is intentionally local and deterministic so the complete commerce flow can run without third-party accounts.
flowchart LR
Domain[Application/domain service]
Port[Application-owned interface]
Mock[Mock or console adapter]
Real[Future real adapter]
SaaS[Provider API]
Domain --> Port
Port --> Mock
Port --> Real
Real --> SaaS
| Concern | Port responsibilities | Default | Intended adapters |
|---|---|---|---|
| IAM | introspection, profile lookup, organizations, grants, capability assignment | self-hosted ZITADEL | ZitadelIamProvider |
| Payment | create, verify, query and refund a payment | mock | Razorpay, Cashfree |
| Shipping | serviceability, rates, shipment/AWB, pickup, tracking, cancellation, return | mock | Shiprocket |
| Settlement/payout | create/query payout using idempotency reference | mock | provider-supported marketplace payout rail |
| render/send transactional email | console/mock | Resend, SES | |
| SMS | send transactional text/OTP | console/mock | MSG91, Twilio |
| KYC | create/query an external verification | mock/reference only | compliant KYC vendor |
| Media | accept metadata/provider references | local/reference only | Cloudinary, S3-compatible storage |
| Search | keyword/filter/sort published catalog | PostgreSQL-backed public queries; in-memory port adapter available | PostgreSQL full-text, Algolia, Typesense, Meilisearch, OpenSearch |
| Support | associate and sync external case reference | local/reference only | Freshdesk/Zendesk-style adapter |
The repository does not contain real provider credentials and does not process raw card data. Empty real-provider variables in .env.example are documentation placeholders, not active implementations.
Modules depend on application tokens rather than provider classes. The global ZitadelModule binds IAM_PROVIDER to ZitadelIamProvider; ordinary integrations use PAYMENT_PROVIDER, SHIPPING_PROVIDER, SETTLEMENT_PROVIDER, EMAIL_PROVIDER, SMS_PROVIDER, KYC_PROVIDER, MEDIA_PROVIDER, SEARCH_PROVIDER, and SUPPORT_PROVIDER. Business modules therefore do not import ZITADEL transport types or third-party SDK models.
The mock webhook adapters use WEBHOOK_MOCK_SECRET. The signature is the lowercase hexadecimal HMAC-SHA256 of the exact raw payload; an optional sha256= prefix is accepted. Test helpers can sign payloads, but bypassing verification is not a supported development shortcut. Real adapters need their own signature algorithm, timestamp/replay checks, event mapping, and verification call where the provider requires it.
Redis-backed jobs are used only when retry, delay, or isolation from the request latency is useful. Representative workloads are:
- transactional notification delivery;
- unpaid inventory reservation expiry;
- retryable provider follow-up;
- periodic payment/settlement reconciliation;
- settlement eligibility evaluation;
- retryable vendor-organization/project-grant and staff capability provisioning.
Workers currently run inside the same NestJS process as the HTTP API. This preserves one deployable modular monolith, but API replica count also controls worker concurrency. A future deployment may add a separate entry point from the same codebase when independent worker scaling or failure isolation is justified.
The concrete queues and job names are:
| Queue | Job |
|---|---|
notification-delivery |
notification.send |
inventory-reservation-expiry |
inventory.reservation.expire |
payment-reconciliation |
payments.reconcile |
settlement-evaluation |
settlements.evaluate |
iam-provisioning |
iam.provisioning.process |
Jobs carry stable IDs and only the minimum delivery data needed (for example notification recipient/content), never entire mutable aggregates, credentials, or raw provider secrets. A worker reloads authoritative state from PostgreSQL and verifies preconditions where the job changes commerce state. Each external side effect has a deterministic job/provider idempotency key. Default jobs attempt at most five times with exponential backoff; successful jobs are retained for a bounded period/count and failed jobs longer for diagnosis.
When ENABLE_QUEUES=false, registered non-delayed handlers execute once inline; delayed jobs and scaffold queues without registered handlers are safely skipped. This makes focused tests independent of Redis, but it does not preserve delayed scheduling: a reservation-expiry job is not retained. Environment validation therefore rejects disabled queues in production; keep them enabled for the complete checkout lifecycle.
Recommended policy for provider jobs is bounded exponential backoff with useful structured failure logs. Permanent validation errors should fail without endless retry. Production operations must alert on exhausted jobs and monitor Redis persistence/capacity.
An in-process event communicates that committed state changed (PaymentCaptured, OrderConfirmed, ShipmentDelivered, and similar). A listener may synchronously update an immediately related read model or enqueue a durable job. Publishing an event does not make an uncommitted transaction safe, and BullMQ is not used as a replacement for database constraints.
- Implement the existing port in
src/integrationswithout importing provider types into domain modules. - Translate money, statuses, errors, and IDs at the adapter boundary.
- Add typed, validated configuration and secret injection.
- Implement signature verification and webhook event mapping if applicable.
- Add contract tests against fixtures/sandbox behavior, including duplicates and timeouts.
- Register adapter selection through configuration and update
.env.example/operations documentation. - Add reconciliation before enabling production money movement.
Provider credentials belong in the deployment secret store, never in Git, image layers, job payloads, audit metadata, or logs.