Skip to content

0010. Enforce tenant isolation on sync rules

Date: 2026-07-27 Status: Accepted

Context

ADR-0005 validates a SyncRule against the direction matrix — can the source export the domain, can the target receive it — but says nothing about who owns either connection. Neither that decision nor the work that implemented SyncRule considered what happens when a rule's source and target SystemConnections belong to different Tenants. The direction matrix has no opinion on this: a source that can pull_in orders and a target that can push_out orders satisfy ADR-0005 regardless of which tenant either connection belongs to. Left unchecked, this would let one company's data be wired into another company's connection — exactly the kind of cross-tenant leak the Tenant scope exists to prevent (see Tenant Model — Function 2). This surfaced during implementation review, not in the original design.

Decision

SyncRule's saving() hook rejects an active rule whose source and target connections resolve to different tenant_ids, throwing InvalidSyncRuleDirectionException::forCrossTenantRule(). This check runs alongside the ADR-0005 direction check, in the same hook, under the same bypass rule: both checks only run when the row being saved has is_active = true, so a rule can be staged as an inactive draft — even a cross-tenant one — without being rejected, and can always be safely deactivated regardless of its current field values.

Tenant isolation is enforced at the model layer rather than deferred to a future Form Request or policy, for the same reason the direction check is: there is no HTTP layer for SyncRule yet, and whatever layer is added later should not be the only thing standing between a cross-tenant rule and the database.

Consequences

A genuine future need to share data across tenants (if one ever arises) requires a deliberate new decision to relax this constraint — it will not happen by accident through an under-specified rule.