Skip to content

Multi-connection orgs β€” eng sizing (before contract)

Requested by: Aurora requirements doc, task β€œeng-sizing” β€” size the real (non-demo) build before signing, since the POC ships on a simulated single-connection org.

[!NOTE] Closed by multi-connection routing This sizing document has been superseded and closed by the general multi-connection ERP routing architecture (Multi-Connection Organizations).

Scope: the three gaps in the requirements doc’s β€œreal gap” table β€” relax the one-active-connection constraint, add content-based routing at intake, add cross-connection order transfer β€” plus the GP write path.

1. Relax erp_connections_active_org_unique (foundation β€” blocks the other two)

Section titled β€œ1. Relax erp_connections_active_org_unique (foundation β€” blocks the other two)”

packages/db/src/schema/erp-connections.ts:290-292 is a partial unique index on clerkOrganizationId where active = true. Verified call-site audit:

Layer Finding
getOrganizationDefaultERPConnection / getOrgScopedConnectionOrDefault 17 call sites across pages + API routes (apps/webapp/src/utils/erp-connections.ts + 16 callers) β€” every one assumes β€œthe org’s connection,” singular. Each needs a disambiguation input (site param, alias-resolved connection, or explicit picker) once the index is relaxed.
getOrganizationERPConnections (the findMany, multi-aware) Zero callers outside its own definition file. The requirements doc’s claim that β€œthe app layer is multi-connection aware” is aspirational β€” the multi-connection helper exists but nothing calls it yet.
Dagster sync (apps/dagster/erp_pipeline/partitions.py:41-90) Already multi-connection-safe β€” get_active_erp_connections() queries WHERE active = true with no org grouping and partitions by connection_id, not by org. Lower risk than the doc assumed; verify but don’t expect rework here.
Billing (apps/webapp/src/pages/api/billing/*) Keyed by clerkOrganizationId, not connection β€” needs a read of whether usage/seat billing should stay org-level (likely yes: Aurora is one customer relationship) or split per site. Product decision, not just eng.

Estimate: 3-4 days. Mechanical (add a required disambiguator to the 17 call sites + one migration to change the unique index shape), but every call site needs a manual read to pick the right disambiguation strategy β€” not a scripted find/replace.

Nothing today decides β€œwhich connection” from document content β€” email_aliases already carries a NOT NULL erpConnectionId per alias (email-aliases.ts:35-37, aliasType='branch' already modeled), so alias-based routing works with zero new schema. The gap is deciding the connection when mail lands on one shared address instead of a per-site alias.

Two extension points already exist and should carry this, not a new concept:

  • inbox_routing_rules (packages/db/src/schema/inbox-routing-rules.ts) β€” org-scoped triggerβ†’action engine with jsonb configs + priority, currently used for no-action-timeout β†’ reassign. A new trigger type (extracted_field_match or similar) evaluated against ship-to/vendor-entity/item-family would fit without a parallel rules table.
  • Fallback for ambiguous cases: LLM proposal + abstain-to-review, same shape as the existing extraction abstention path.

Estimate: 1-1.5 weeks for rules-based routing (new trigger type + evaluator + UI to author rules) once (1) is in place. LLM-fallback routing is a second phase, not needed for GP-only Aurora given how distinct their per-site signals already are (vendor entity name, ship-to state).

Corrected 2026-07-22: extracted_orders carries no connection column at all β€” verified against apps/webapp/src/lib/inbox/erp-connection-sql.ts. The effective connection is resolved live via coalesce(emailAliases.erpConnectionId, pdfDocuments.erpConnectionId): the alias wins whenever the item arrived through one (every email_aliases row has a NOT NULL erpConnectionId), so a naive UPDATE pdf_documents SET erp_connection_id = ... silently no-ops for the common alias-sourced case. This is also why SupervisorKanban.tsx:316,506 explicitly disables dragging in the connection lens today, with a comment noting the connection is β€œresolved from ingestion… not something a supervisor reassigns.”

Correct approach: a new nullable erpConnectionIdOverride on pdf_documents, with the coalesce extended to check it first. A transfer sets the override on the one document being moved β€” it never touches the alias, so it can’t reroute future mail through that alias by accident. Needs an audit-trail table either way (who/when/ from/to/reason) and lifting the SupervisorKanban drag-disable for flagged orgs only. Governance question (open item #7 in the requirements doc) still applies: who’s authorized to move an order, and which ERP is source of truth mid-transfer.

Estimate: still ~1 week β€” the corrected design (new override column + coalesce change + audit table + UI unlock) is roughly the same shape of work as the original re-parent estimate, just aimed at the right column.

Not sized here. eConnect/Web Services vs. file-drop, times up to 7 on-prem GP instances with presumably 7 different network/credential postures, is a discovery task before it’s an estimate. IQMS (2 instances) is explicitly out of scope per the requirements doc (roadmap gesture, not POC/early-implementation).

Phase Estimate Blocks
1. Relax active-connection index 3-4 days 2, 3
2. Content-based routing (rules-first) 1-1.5 weeks β€”
3. Cross-connection transfer 1 week governance answer (open item #7)
4. GP write path unscoped β€” needs a discovery spike per site β€”

~3 weeks of eng for (1)-(3) before any GP write integration. This is a scoping input for the contract conversation, not a commitment β€” phase 4 in particular can’t be bounded until Aurora’s IT lead engages on per-site GP access.