Skip to content

Integration Architecture Pattern: Temporal-Based Durable Transactions

Date: March 6, 2026
Status: ESTABLISHED PATTERN
Safety Level: CRITICAL


All third-party ERP integrations should follow the Temporal-based durable transaction pattern:

  • Read Operations (GET) β†’ Temporal activities (cacheable reference data) + Dagster batch jobs
  • Write Operations (POST/PATCH/DELETE) β†’ Temporal workflows with retry logic, audit trails, and rollback capability
  • Connector Layer (@repo/integrations): TypeScript ERP connectors implement packages/integrations/src/core/types.ts and packages/integrations/src/core/base-connector.ts, registered in packages/integrations/src/registry/registry.ts (service.ts) with concrete implementations under packages/integrations/src/connectors/.
  • Rationale: Ensures reliability, traceability, and atomicity across network boundaries

This document establishes the pattern once and prevents reinventing the wheel for P21, NetSuite, SAP, etc.


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ WEBAPP USER ACTION β”‚
β”‚ (e.g., "Submit Order to P21") β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TEMPORAL WORKFLOW (Durable Transaction) β”‚
β”‚ β”‚
β”‚ 1. Fetch live data from ERP API (GET) β”‚
β”‚ 2. Validate against local rules β”‚
β”‚ 3. Calculate adjustments (pricing, discounts, etc) β”‚
β”‚ 4. Show user confirmation β”‚
β”‚ 5. On approval: β”‚
β”‚ β”œβ”€ Begin transaction β”‚
β”‚ β”œβ”€ Send POST/PATCH to ERP β”‚
β”‚ β”œβ”€ Retry on failure (exponential backoff) β”‚
β”‚ β”œβ”€ Rollback if failed β”‚
β”‚ └─ Log audit trail β”‚
β”‚ 6. Return result (success/failure) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β–Ό β–Ό β–Ό
SUCCESS FAILURE AUDIT LOG
(return) (retry) (store)

Aspect Temporal Advantage
Durability Survives network failures, worker crashes
Retry Logic Exponential backoff with configurable limits
Audit Trail Every step logged for compliance
Rollback Can reverse failed writes (compensating transactions)
Timeout Handling Won’t hang indefinitely on bad connections
Ordering Guarantees steps execute in order
Idempotency Safe to replay without side effects

Status: βœ… READY FOR IMPLEMENTATION

WEBAPP
└─ User submits order
β”‚
β–Ό
TEMPORAL WORKFLOW: submit_order_to_wherefour
β”‚
β”œβ”€ Activity 1: fetch_order_from_api(order_id)
β”‚ └─ GET /orders/{id}
β”‚ └─ Returns: order_items[], custom_line_items[], pricing
β”‚
β”œβ”€ Activity 2: get_pricing_tiers_from_cache()
β”‚ └─ Read Redis (24h TTL, synced by Dagster)
β”‚
β”œβ”€ Activity 3: validate_order(order, tiers)
β”‚ └─ Pure Python: validate products + custom items
β”‚
β”œβ”€ Activity 4: calculate_discount(order, validation)
β”‚ └─ Apply tier discounts + allowances
β”‚ └─ Return discount chip data
β”‚
β”œβ”€ (User confirms in UI)
β”‚
β”œβ”€ Activity 5: submit_to_wherefour(order)
β”‚ └─ ⚠️ ONLY on Sandbox account (when available)
β”‚ └─ BLOCKED for production until sandbox exists
β”‚
└─ Activity 6: audit_log(workflow_id, status)
└─ Store in PostgreSQL audit_trail table
Data Operation Source Where
Orders GET API Temporal (on-submit)
Order Items GET API Temporal (on-submit)
Custom Line Items GET API Temporal (on-submit)
Pricing Tiers GET Scraper Redis (Dagster daily)
Customers GET API Redis (Dagster daily)
Inventory GET API PostgreSQL (Dagster daily)

⚠️ CRITICAL SAFETY BOUNDARY: NO WRITES TO PRODUCTION

Section titled β€œβš οΈ CRITICAL SAFETY BOUNDARY: NO WRITES TO PRODUCTION”
# ALLOWED (GET only)
βœ… fetch_order_from_api(order_id)
βœ… get_customers_list()
βœ… get_inventory_catalog()
# BLOCKED (POST/PATCH/DELETE)
❌ submit_to_wherefour() # ← Wait for sandbox account
❌ update_order_status() # ← Wait for sandbox account
❌ create_invoice() # ← Wait for sandbox account

RULE: If operation requires POST, PATCH, or DELETE, it MUST target sandbox account first.
STATUS: Sandbox account NOT YET AVAILABLE.
IMPLICATION: CK POC cannot test write operations until sandbox confirmed.


Status: PLANNED (Q2/Q3 2026)
Scope: Refactor existing P21 integration to use Temporal pattern

  • Identify all P21 API calls in codebase
  • Classify: GET (read) vs POST (write)
  • Document current error handling
  • Identify silent failure points
Temporal Workflow: submit_order_to_p21
β”œβ”€ Activity: fetch_p21_customer(customer_id)
β”‚ └─ GET /customers/{id} from P21 API
β”‚
β”œβ”€ Activity: validate_p21_sku_mapping(order_items)
β”‚ └─ Check SKU translations
β”‚
β”œβ”€ Activity: submit_p21_purchase_order(order_data)
β”‚ └─ POST /purchase-orders to P21 (with retry)
β”‚
β”œβ”€ Activity: confirm_p21_receipt(po_id)
β”‚ └─ PATCH /purchase-orders/{id}/confirm
β”‚
└─ Activity: log_audit_trail(workflow_id, p21_po_id)
└─ Store in PostgreSQL
  1. Phase 1: Extract P21 API client library (if doesn’t exist)
  2. Phase 2: Create Temporal activities for core operations
  3. Phase 3: Create Temporal workflows (submit order, confirm, cancel, etc)
  4. Phase 4: Integrate with webapp (replace direct P21 calls)
  5. Phase 5: Add retry logic + exponential backoff
  6. Phase 6: Add audit logging + dashboard

Apply same pattern to:

Temporal Workflows:
β”œβ”€ submit_order_to_netsuite
β”œβ”€ sync_inventory_from_netsuite
└─ pull_customer_data
Read operations (Dagster):
β”œβ”€ Fetch customers daily
β”œβ”€ Fetch tax codes daily
└─ Fetch item catalog daily
Temporal Workflows:
β”œβ”€ submit_sto_to_sap
β”œβ”€ confirm_receipt
└─ sync_shipment_status
Read operations (Dagster):
β”œβ”€ Fetch material master data
β”œβ”€ Fetch cost centers
└─ Fetch plant data
Temporal Workflows:
β”œβ”€ create_invoice_in_qb
β”œβ”€ record_payment
└─ sync_estimates
Read operations (Dagster):
β”œβ”€ Fetch customers
β”œβ”€ Fetch items
└─ Fetch payment methods

Dagster jobs handle read-only, batch operations ONLY:

@job
def wherefour_sync_daily():
"""Sync reference data (NOT transactional)."""
# Fetch reference data
tiers = scrape_pricing_tiers() # Playwright scraper
customers = fetch_customers_from_api() # GET /customers
inventory = fetch_inventory_from_api() # GET /inventory
vendors = fetch_vendors_from_api() # GET /vendors
# Cache in Redis + PostgreSQL
cache_pricing_tiers(tiers) # Redis 24h TTL
cache_customers(customers) # Redis 24h TTL
store_inventory(inventory) # PostgreSQL 7d retention
store_vendors(vendors) # PostgreSQL 7d retention

NEVER use Dagster for writes (POST/PATCH/DELETE). Use Temporal instead.


βœ… OK: GET /customers, GET /inventory, GET /orders
❌ NO: POST /invoices, PATCH /status, DELETE /orders
βœ… OK: Temporal activity submits POST to sandbox
❌ NO: Direct httpx.post() in main code
❌ NO: Direct requests.post() in webapp
Before testing ANY POST/PATCH/DELETE:
1. Request sandbox account from vendor
2. Verify credentials work
3. Run test against sandbox
4. Document test results
5. ONLY then: enable production writes
Every POST/PATCH/DELETE must:
β”œβ”€ Log to PostgreSQL integration_audit_trail
β”œβ”€ Include workflow_id + timestamp
β”œβ”€ Include full request payload
β”œβ”€ Include full response payload
└─ Be queryable for compliance

In TypeScript this is not hand-rolled per adapter. Wrap the transport call in auditErpWrite (or auditErpFetch, for adapters whose transport returns a Response) from @repo/integrations/services. Wrapping at the transport seam, not at the caller, is the point: Eclipse E4 resolves item identities, revisions and feature flags inside createOrder, so the input the caller passed is not what was sent. The helper redacts secrets centrally, caps stored payloads at 200k characters, and never throws, so a broken ledger cannot turn a landed ERP write into a failed one. The contract is stated in the IERPService docblock (packages/integrations/src/services/ierp-service.ts); wired today for eclipse-e4, wherefour and prophet21.

Retention: rows are deleted after 90 days by pnpm --filter @repo/db purge-integration-audit-trail (RETENTION_DAYS overrides, DRY_RUN=1 previews), available as a task in the db-maintenance GitHub Actions workflow.


Illustrative. The canonical definition is packages/db/src/schema/integration-audit-trail.ts, which also carries workflow_type, organization_id, entity_type, entity_id, external_id and elapsed_ms.

CREATE TABLE integration_audit_trail (
id BIGSERIAL PRIMARY KEY,
workflow_id TEXT, -- Temporal workflow ID; nullable, and TEXT not
-- UUID: workflow ids here are strings
-- ("erp-submission-<orderId>") and a write issued
-- straight from the webapp has no workflow at all
integration_name VARCHAR(50), -- "wherefour", "p21", "netsuite"
operation VARCHAR(20), -- "GET", "POST", "PATCH", "DELETE"
endpoint VARCHAR(255), -- "/orders", "/customers/123"
status VARCHAR(20), -- "SUCCESS", "FAILURE", "RETRY"
request_payload JSONB, -- What we sent
response_payload JSONB, -- What we got back
error_message TEXT, -- If failed
attempted_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_audit_workflow_id ON integration_audit_trail(workflow_id);
CREATE INDEX idx_audit_integration ON integration_audit_trail(integration_name);
CREATE INDEX idx_audit_timestamp ON integration_audit_trail(created_at DESC);

Endpoint Method Purpose Where Used Cache TTL
/customers?limit=100 GET Customer list Dagster job 24h
/customers/{id} GET Customer details Temporal (on-demand) 24h
/orders?limit=100 GET Orders list Dagster snapshot -
/orders/{id} GET Order details + items Temporal (on-submit) 5min
/inventory?limit=100 GET Inventory catalog Dagster job 7d
/inventory/search?q=X GET Search products Webapp route -
/vendors?limit=100 GET Vendor list Dagster job 7d
/settings/price_tiers WEB SCRAPE Pricing tiers Dagster + Playwright 24h
  • ❌ POST to /orders
  • ❌ PATCH to /order/{id}
  • ❌ DELETE to /order/{id}

Reason: No sandbox account available. Production data protection.

  • βœ… All GET endpoints verified (11/11 passing)
  • βœ… Order structure validated (order_items + custom_line_items)
  • βœ… Pricing data verified (subtotal, discount, total)
  • βœ… Search endpoints confirmed working
  • ❌ Write operations: NOT TESTED (blocked by safety rule)

Scenario Without Temporal With Temporal
Network fails mid-submit ❌ Data lost βœ… Retry automatically
Worker crashes ❌ Workflow lost βœ… Resumed on recovery
Invalid data submitted ❌ Silent failure βœ… Logged + auditable
Need to replay transaction ❌ Manual intervention βœ… Replay from workflow ID
Compliance audit ❌ No trail βœ… Full audit log
Production issue ❌ Hard to debug βœ… Queryable workflow history

  1. βœ… WhereFour CK POC: Implement Temporal workflow for order submission (GET only)
  2. πŸ”² P21 Refactor: Create detailed project plan using this pattern (Q2/Q3)
  3. πŸ”² NetSuite: Update project plan to use Temporal (when started)
  4. πŸ”² SAP: Update project plan to use Temporal (when started)
  5. πŸ”² QuickBooks: Update project plan to use Temporal (when started)

Do not start P21/NetSuite/SAP refactors yet. This establishes the pattern for when you do.


Before implementing any integration write operations:

  • Sandbox account requested and confirmed
  • Test data loaded into sandbox
  • Temporal workflow created with retry logic
  • Audit trail table created
  • Rollback strategy documented
  • Error handling tested
  • Production credentials secured (1Password / Infisical)
  • Approval from product owner
  • Gradual rollout plan created (5% β†’ 25% β†’ 100%)

FINAL WARNING: 🚨 NO WRITES TO WHEREFOUR PRODUCTION 🚨
Sandbox account only. GET operations only. Await explicit approval before enabling POST/PATCH/DELETE.