Skip to content

Order-Automation Integration Playbook — Design

Status: APPROVED as debt-consolidation; PR1 (auth/encryption forks) shipped via #1733. PR2 (impl move + factory dedup) and PR3 (contract test) deferred. Branch: claude/modest-swirles-88f861 PR base: staging

Reader note: the original “Proposal” and “Phasing” sections below describe the doc’s ORIGINAL framing (self-host Nango first, new manifest, fast-onboarding claim). All three were CUT at the /autoplan review gate — see “GSTACK REVIEW REPORT” → “REVIEW RESOLUTION” near the end of this file for the approved scope, and “PR1 EXECUTION LOG” for what actually shipped vs what the review predicted. The proposal sections are kept for historical context, not as current direction.

Adding or operating an order-automation ERP connector is not a repeatable process. The write path (IERPService) lives inside apps/ts-temporal-worker where only Temporal can reach it; the read path lives in apps/dagster; credentials resolve per-ERP through erp_auth.py (Python) and ERPAuthService (TS) against Infisical. There is no single contract, no single credential layer, and no checklist. Every new ERP re-derives the wiring, and the same write logic can’t be reused by ops-mcp or the webapp without copy-paste.

Make order automation a repeatable playbook across the connectors we already have: one action contract, one credential layer, one checklist. Adding an ERP becomes “implement 3 reads + 1 write, register creds in Nango, fill the manifest.”

Non-goals (explicitly out of scope):

  • AR/AP, accounting, invoicing, ecommerce connectors (deferred platform expansion).
  • Replacing Dagster/DLT (the read runtime) or Temporal (the orchestration runtime).
  • Nango’s syncs/actions runtime (enterprise-only on self-host; no templates for our niche ERPs anyway).
  • The customer-data-workbench (separate, deferred design).

The order-automation surface (the entire contract)

Section titled “The order-automation surface (the entire contract)”

Every order-automation ERP needs exactly this — nothing more for v1:

Half Operations Runtime Destination
Read / Sync customers, products, shipping_addresses Dagster + DLT (Python) R2 Iceberg → Typesense
Write / Action createOrder (+ existing approveQuote, updateOrder, filterInvalidItems) IERPService (TS) the ERP API
Auth credential storage + proxy Nango (self-hosted) —

In production only createOrder hits the ERP directly; all reads are served from Typesense (faster, more configurable). The playbook formalizes that split.

1. Promote the write contract into a shared package (DRY)

Section titled “1. Promote the write contract into a shared package (DRY)”

getERPService() + the per-ERP IERPService implementations move out of apps/ts-temporal-worker/src/services/ into packages/integrations (where the IERPService interface already lives). Then every surface is a thin caller:

  • Temporal createOrderActivity → getERPService(conn).createOrder(input)
  • ops-mcp order tool → same
  • webapp API route → same

The package is the home of the logic; MCP/Temporal/webapp are transports. No surface owns write logic.

2. One credential layer: Nango self-hosted (Auth + Proxy)

Section titled “2. One credential layer: Nango self-hosted (Auth + Proxy)”

Self-host Nango on the existing GCP footprint. Free tier = Auth + Proxy only (Functions/syncs/actions runtime is enterprise — we don’t use it; no templates exist for our niche ERPs regardless). Nango becomes:

  • The single credential vault for every order-automation connection (replaces per-ERP Infisical paths + erp_auth.py + ERPAuthService).
  • An authenticated proxy for outbound ERP HTTP — connector code never handles secrets (satisfies the “never handle secrets” rule structurally).
  • Later (not v1): the embeddable Connect UI for customer self-serve onboarding.

Self-host components: Postgres (credential store), Redis (cache), Server (Node), Runner (Node — deployed but idle, no Functions). Elasticsearch optional → skip. NANGO_ENCRYPTION_KEY (base64 32-byte) in 1Password, sourced via op. No key rotation support — escrow deliberately. The Nango Postgres becomes the single source of truth for credentials → backups + blast-radius treatment.

Proxy is HTTP-only. Eclipse E4 reads are MSSQL via pyodbc, not HTTP — Nango is the vault for Eclipse creds (getConnection() → feed pyodbc) but not the transport. HTTP ERPs (P21, WhereFour, Business Edge, NetSuite) use full proxy.

To add an order-automation ERP:

  1. Register the ERP in the connector manifest (auth mode, capabilities, Nango ref).
  2. Implement 3 Dagster sync methods: sync_customers, sync_products, sync_shipping_addresses (the existing WhereFour orchestrator is the reference).
  3. Implement IERPService (createOrder at minimum) in packages/integrations.
  4. Register credentials in Nango.
  5. Fill the connector manifest entry; CI verifies the 4 above exist.

Conformance scorecard (what we already have)

Section titled “Conformance scorecard (what we already have)”
ERP reads (3) createOrder creds in Nango gaps
Prophet21 ✓ ✓ migrate from Infisical OData v3/v4 complexity (existing)
WhereFour ✓ ✓ currently Nango-managed reference impl
Eclipse E4 partial ✓ (SOAP) vault-only (MSSQL not proxied) reads are pyodbc
Business Edge planned planned DevKey via proxy net-new (see leverage-catalog)
NetSuite partial target OAuth via Nango OAuth lifecycle = Nango’s strong case
  1. Nango self-host (Auth+Proxy) for all connections — pure consolidation, no behavior change. Kills per-ERP credential code.
  2. Promote getERPService + impls to packages/integrations — rewire Temporal, ops-mcp, webapp as thin callers.
  3. Write the manifest + CI conformance check — the playbook becomes enforceable.
  4. Backfill each existing connector to conformance as it’s next touched.

Monorepo (pnpm). IERPService interface in packages/integrations/src/services/. Factory currently in apps/ts-temporal-worker/src/services/erp-service-factory.ts. Dagster sync orchestrators in apps/dagster/erp_pipeline/implementations/. Reads → R2 Iceberg → Typesense. Nango self-hosted on GCP. PR base = staging.


/autoplan — CEO + Eng + DX (Design skipped: no UI). Dual voices: codex + Claude subagent each phase. DX emphasized.

  • Impls live in apps/webapp/src/services/erp/implementations/, NOT ts-temporal-worker. Temporal imports them via a #webapp/* esbuild alias hack (TODO in erp-service-factory.ts literally says “Move ERP service implementations to @repo/integrations”).
  • erp-auth.ts, encryption.ts, CredentialService, IERPService ALREADY exist in packages/integrations — but as divergent dead copies of the live webapp versions.
  • Nango is already in use (WhereFour on Nango Cloud); Connect UI work is in-flight on another branch. The doc proposed self-host “as if greenfield.”
  • A connector registry already exists: packages/integrations/src/registry/registry.ts. Dagster capabilities is a second source. The proposed MANIFEST would be a third.
Dimension Claude Codex Consensus
Connector velocity = the bottleneck? NO (customer acq + data quirks) NO (1-2 ERPs/yr) CONFIRMED: premise weak
Self-host Nango justified? NO (already on Cloud; SPOF) NO (vault+proxy only; debt) CONFIRMED: defer
Promote write package = real win? YES (live #webapp hack + TODO) YES if 2+ consumers CONFIRMED: the keeper
Extraction is drop-in? NO (divergent auth/enc forks) NO (import drag) CONFIRMED: reconcile first
5-step checklist real? aspirational days-weeks not hours CONFIRMED: right-size
New manifest? rots, redundant 3rd source of truth CONFIRMED: cut, reuse registry
Conformance CI value type+contract test dumb existence only CONFIRMED: existence + 1 contract test

CRITICAL finding (Eng — production landmine)

Section titled “CRITICAL finding (Eng — production landmine)”

packages/integrations/src/auth/erp-auth.ts is a divergent copy of the LIVE apps/webapp/src/services/erp-auth.ts. Live P21 does decrypt(connection.password); the package version does CredentialService().getCredentials() (Infisical). Moving Prophet21Service to import the package version silently reroutes P21 auth → production order-submission break. createOrderActivity (erp-writes.ts:91) is a hot path. Same fork exists for encryption.ts (warn-vs-throw) and the getERPService factory (webapp + worker = duplicate copies).

# Phase Decision Class Principle Rationale
1 CEO Reframe: this is debt-consolidation (kill forks + #webapp hack), not a connector-velocity platform PREMISE P6 Both: velocity isn’t the bottleneck; don’t oversell
2 CEO Defer self-host Nango USER CHALLENGE P3/P4 Both: already on Nango Cloud + Infisical works; self-host = non-rotatable-key SPOF for just vault+proxy
3 Eng Reconcile auth/encryption/factory forks FIRST (no-behavior-change PR, verified vs staging P21+WhereFour) AUTO P1/security Both: silent auth reroute = prod break
4 Eng Strangler migration (copy+rewire impls → package factory → repoint temporal, verify createOrder → repoint webapp → remove #webapp alias LAST). Not big-bang AUTO P5 Both gave near-identical sequence
5 Eng Validate new transitive deps via check-external-deps.mjs (fuzzysort, jsonwebtoken, @repo/observability) AUTO P1 else worker ERR_MODULE_NOT_FOUND on boot
6 Eng/DX Keep extracted package RUNTIME-THIN (ops-mcp is a CF Worker; integrations pulls @repo/db/ioredis/AWS) AUTO P5 Both: fat package = worse coupling
7 DX CUT the new MANIFEST; reuse registry.ts AUTO P4 Both: redundant 3rd source, fills twice
8 DX Right-size claim: saves hours-to-days, not an order of magnitude; new ERP still days-weeks (Dagster reads dominate) AUTO P6 Both: SOP says 4-8 weeks
9 DX Conformance CI = existence check + ONE shared contract test both consumers run AUTO P3 stronger = paperwork theater
10 Eng Trim IERPService order-automation contract (createItem/update/delete are dead weight, non-optional) AUTO P5 else conformance forces stub methods
  • T1: The keeper is the package extraction; everything else is speculative. All phases.
  • T2: Self-host Nango is premature. CEO (SPOF/debt) + DX (skip it). Already on Cloud.
  • T3: Don’t add new sources of truth (manifest) on top of existing ones (registry.ts, Dagster capabilities). DX both.

User decisions at the /autoplan gate:

  1. Scope → CONSOLIDATE + THIN PACKAGE + CONTRACT TEST. Drop the self-host Nango migration and the new manifest from this work. The approved deliverable:

    • PR 1 (no behavior change): reconcile the divergent forks — one ERPAuthService, one encryption.ts, one getERPService factory. Point the LIVE webapp at the consolidated versions. Verify P21 + WhereFour auth/createOrder against staging before merge. This removes the silent-auth-reroute landmine.
    • PR 2 (strangler): move the IERPService impls into a runtime-thin @repo/integrations (must stay importable by ops-mcp, a Cloudflare Worker — no @repo/db/ioredis/AWS in the hot path). Add getERPService as a package export. Repoint the Temporal factory first, verify createOrder; then webapp; remove the #webapp/* esbuild alias LAST. Validate new transitive deps (fuzzysort, jsonwebtoken, @repo/observability) via check-external-deps.mjs.
    • PR 3 (guardrail): one shared contract test suite both webapp + ts-temporal run, asserting each registered ERP satisfies the order-automation contract. Conformance CI = existence check, not a heavy gate. Reuse registry.ts; no new manifest.
    • Trim the order-automation contract: createItem/updateItem/deleteItem are dead weight (non-optional today) — make optional or drop for this surface.
  2. Nango self-host → DEFERRED. Stay on Nango Cloud (WhereFour) + Infisical. Revisit self-host at a real trigger: actually needing Nango’s syncs/actions runtime, or shipping customer self-serve onboarding (the Connect-UI work already in flight). Rationale: skipping the runtime, self-host buys only vault+proxy while adding a non-rotatable-key single-point-of-failure for all ERP creds.

  3. Framing corrected: this is debt consolidation (kill the duplicate forks + the #webapp hack), not a connector-velocity platform play. Net-new ERP onboarding stays days-to-weeks (Dagster reads dominate); this saves hours-to-days on the write side and removes a production landmine.

Status: design APPROVED as debt-consolidation (PR1→PR2→PR3); self-host Nango and manifest CUT; new-ERP “fast onboarding” claim withdrawn.


PR1 EXECUTION LOG (what changed from the review prediction)

Section titled “PR1 EXECUTION LOG (what changed from the review prediction)”

PR1 shipped as #1733 on claude/modest-swirles-88f861. Three commits, -160 lines of duplicated code, +106 lines of tests + comments (net -54). Pre-push hook (full monorepo lint + build) clean.

Commit What
3660b6788 encryption fork unified (canonical 100k, frozen-ciphertext regression test)
c19b82dec ERPAuthService fork unified (Infisical-first for P21, parity-verified)
6569fd354 review fixes (JSON-parse guard + name all 5 PBKDF2 forks)

Facts that emerged during execution (update the doc, not just the resolution)

Section titled “Facts that emerged during execution (update the doc, not just the resolution)”
  1. The encryption fork was a LANDMINE, not just duplication. Webapp’s encryption.ts derived keys at 100,000 PBKDF2-SHA512 iterations; the package’s was at 210,000. Different iterations → different derived keys → ciphertext from one cannot be decrypted by the other. The review described it as “warn-vs-throw”; the reality was production-outage-on-cutover if anyone consolidated naively. Empirically verified: 100k round-trips, 210k throws on 100k ciphertext. Every stored ciphertext is at 100k (nothing calls the package encrypt() to persist — CredentialService writes plaintext JSON to Infisical). So 100k was the only safe canonical choice.

  2. All 7 staging P21 connections are Infisical-backed (secrets_provider='infisical'), including active Genfit Playground and P21 Testing. They ALSO carry inline ciphertext, but the LIVE webapp ERPAuthService only ever read the inline column — Infisical was already populated but unused for P21 auth. Staging parity check via the infisical CLI (against infisical.ordermatic.co) confirmed inline-decrypted password ≡ Infisical-stored password for all 7. The auth-source flip from inline → Infisical was therefore zero-data-change in staging.

  3. Infisical IS now a runtime dependency for P21 auth. This wasn’t an accident — it’s the package version’s behavior (CredentialService → getFromInfisical when secrets_provider='infisical'). User accepted this explicitly at the gate. Caveat for PR2: any failure of Infisical now blocks P21 token issuance at every cache-miss (5-min TTL × N connections). If that blast radius becomes uncomfortable, the right fix is a CredentialService inline-fallback when Infisical is unreachable AND connection.password is set — but defer that until there’s actual operational pain.

  4. The JSON-parse trap was a real latent bug (caught by /code-review, not the original /autoplan). The inline-fallback path in CredentialService ran unconditional JSON.parse(decrypted), which would have silently broken any future inline P21 password that happened to be valid JSON: all-digit (“8675309” → number), boolean (“true”), or an object missing password ({} → empty-password header). Fixed in 6569fd354 with a strict-shape guard. Pinned with 5 regression tests.

  5. Five hand-rolled PBKDF2 sites at 100k, not one. Named in the encryption.ts comment so the next iteration-count bump doesn’t leave them stranded: apps/dagster/erp_pipeline/sources/erp_auth.py, apps/temporal-worker/activities/erp_credentials.py, three TS scripts in apps/webapp/scripts/ and packages/db/scripts/. Any bump needs (a) a re-encryption migration AND (b) coordinated bumps across all 6 sites.

  6. packages/integrations/src/prophet21/transaction-api.ts is dead code — no callers anywhere in the monorepo (only re-exported through packages/integrations/src/index.ts). It imports the package’s ERPAuthService. PR2 should delete it.

The review predicted PR2 would need to “reconcile the auth/encryption/factory forks FIRST.” PR1 did the auth + encryption half. PR2’s remaining scope:

  1. Copy Prophet21Service, WherefourService, EclipseE4Service, NetSuiteService, DemoService from apps/webapp/src/services/erp/implementations/ into @repo/integrations. Import rewrites for the auth/encryption forks are now trivial — both modules already live in @repo/integrations/auth.
  2. Add getERPService as a @repo/integrations export. Repoint Temporal first, verify createOrder against staging P21 (Genfit Playground) and WhereFour (the active inline connection). Then repoint webapp.
  3. Remove the #webapp/* esbuild alias LAST, after both consumers are green.
  4. Validate new transitive deps (fuzzysort, jsonwebtoken, @repo/observability) via apps/ts-temporal-worker/scripts/check-external-deps.mjs — without this, the worker ERR_MODULE_NOT_FOUND on boot.
  5. Delete packages/integrations/src/prophet21/transaction-api.ts (dead).
  6. Test mocks: 3 webapp test files use vi.mock('@/services/erp-auth') — the re-export preserves binding identity, so they keep working. Don’t move them to the package side without re-verifying mock semantics.

PR3 (contract test) and the IERPService trim (drop createItem/update/delete from the non-optional surface) are unchanged from the review resolution.