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.
2. Content-based routing at intake
Section titled β2. Content-based routing at intakeβ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_matchor 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).
3. Cross-connection order transfer
Section titled β3. Cross-connection order transferβ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.
4. GP write path (unscoped β the actual unknown)
Section titled β4. GP write path (unscoped β the actual unknown)β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.