Back to Blog
· Aptli

Contractor Separation: Two Layers Instead of One

When multiple parties share a platform — staff, subcontractors, even competing contractors — authentication alone does not isolate them. Here is how we think about building that separation in a way that actually holds up.

authorizationmulti-tenantcontractor-separationsecurity

The Situation

More and more of the organizations we work with run shared platforms where multiple parties operate on the same underlying asset data. A utility might have internal crews, a primary construction contractor, and two or three subcontractors all touching the same network. A municipality might have its own staff plus outside engineering firms. It is common — and increasingly unavoidable — for parties who compete with each other to end up working inside the same application.

The obvious question is how you keep those parties from seeing each other's work. The obvious first answer is "give each of them a login." That gets you authentication, which is necessary but not sufficient. Authentication tells the system who you are. It does not tell the system what you are allowed to see. Without a second layer, everyone who logs in sees everything.

The Approaches That Do Not Quite Hold Up

There are four common approaches, and each has a place. They also each have limits worth being honest about.

Separate tenant deployments. Give each contractor their own copy of the application, their own database, their own environment. This is real isolation and there are cases where it is the right answer — usually when the parties share nothing and will never collaborate. It gets expensive fast, and it defeats the reason the platform exists if the whole point was collaboration, rollup reporting, or a single map of truth. If most of the data is shared and only a slice is sensitive, separate tenants is overkill.

UI-only hiding. The most common shortcut. Hide the button, skip the menu item, filter the list view. This is security theatre. Anyone with browser developer tools, a direct API request, or a well-aimed export can retrieve the "hidden" data. The record still exists, still flows over the wire, and still appears in bulk exports. It keeps honest users from stumbling onto things; it does not stop anyone who is actually looking.

Application-layer filtering, endpoint by endpoint. A real step up: enforce the filter in code wherever data is queried. This works — right up until it does not. Security now lives in dozens of query sites. Every new feature is a potential leak. Every refactor is a chance to forget one. This approach tends to be correct at the moment it ships and to drift out of correctness as the codebase grows.

Exporting to separate workspaces. Copy the data each contractor needs into a workspace only they can see. This is genuinely isolated but the copy goes stale the moment it is made. Now you have a sync problem instead of a visibility problem, and field workers end up looking at yesterday's view of today's work.

None of these are wrong in every situation. They are wrong as a general-purpose answer to multi-party access on a shared platform.

The Two-Layer Model

Our approach separates authorization into two independent layers that compose.

Admin rights are permissive. They describe what a user is allowed to do — create a work order, edit a report, delete a stock item. Without an admin right, the default is that you can view data but cannot alter it.

Role restrictions are restrictive. They describe what a user cannot see or touch at all. Each restriction specifies a model (points, reports, assignments), a field on that model (owner, status, category), a comparison, and a value. Matching records are hidden from members of the role for the specified operations — read, edit, create, delete — independently.

These layers do not interfere with each other. A field worker can hold the right to edit reports while a role restriction hides any report not authored by them — so they can edit, but only their own. The admin right grants the capability; the role restriction narrows the scope. Neither layer needs to know about the other, and that independence is most of the reason the model ages well.

Why Server-Side, Field-Level Enforcement Matters

Role restrictions are applied on the server, before data leaves the database. This is the part that matters in practice:

  • The filter applies to every query path — detail pages, list views, API calls, bulk exports, map tiles.
  • A user who types a record ID they happen to know into the URL gets a 404 Not Found, because to them the record genuinely does not exist.
  • A screenshot from another user's screen is not useful; the data will not load when the restricted user tries to reach it.
  • New features inherit enforcement automatically, because they go through the same server data layer.

UI-level hiding fails every one of these tests. Application-layer filtering passes them only if the developer remembers to apply the filter at each new query site. Field-level restrictions enforced at the server turn the question from "did we remember?" to "did the data match the rule?" — which is the question we actually want the system to be answering.

A Worked Example

Two contractors on the same fibre build. Both use the same map, both consume the same shared base layers, both log reports against their own work.

Create a role named Contractor A with a single restriction:

  • Model: Point
  • Field: owner
  • Comparison: =
  • Filter value: Contractor B
  • Permissions blocked: read, edit, create, delete

Add Contractor A's users as members. Do the mirror for Contractor B. That is the entire configuration.

Contractor A's crews now see their own points, the shared layers, and nothing from Contractor B. If they open a list of points, Contractor B's records are not in it. If they export to CSV, the export is filtered. If they guess the numeric ID of a Contractor B point and paste it into the URL, they get a 404 Not Found. Contractor B experiences the mirror image. Neither side knows how much work the other has done, where it is, or when it was updated.

The shared base layers — cable routes, poles, duct infrastructure — remain visible to both, because no restriction applies to them. Collaboration is preserved where it is wanted; isolation is enforced where it is required. The platform does not have to choose between the two.

When Separate Tenants Are Still the Right Answer

The two-layer model is not a universal answer. If two parties share nothing, never collaborate, never produce a joint report, and have regulatory reasons to live on physically separate infrastructure, then separate tenants is the honest choice. What the two-layer model replaces is the far more common case where parties share most of the platform and need to hide a specific slice. That is the case where separate tenants wastes money, breaks reporting, and slows work down, and where UI-only hiding leaks.

The test we apply is simple: if the parties would benefit from seeing the same base layers, running the same reports, and working inside the same map, they belong on the same platform — with real separation enforced at the level where it actually holds.

Summary

  • Multi-party work on shared platforms is increasingly common in utilities, telecom, and municipal operations, and increasingly includes parties that compete with each other.
  • Authentication alone does not isolate users; it only identifies them. Everyone who logs in still sees everything unless a second layer is added.
  • Separate tenant deployments offer real isolation but defeat the point of a shared platform when parties need to collaborate on most of the data.
  • UI-only hiding is security theatre — the data is still there and retrievable through developer tools, the API, or exports.
  • Application-layer filtering is better but tends to drift out of correctness as the codebase grows and new features are added.
  • The two-layer model separates authorization into permissive admin rights (what you can do) and restrictive role restrictions (what you cannot see), enforced at the server on a field-by-field basis.
  • Server-side field-level enforcement means restricted data genuinely does not exist from the restricted user's perspective — not in the UI, not in the API, not in exports, not at a guessed URL.
  • The worked configuration is a single role per contractor, and the result is isolation where it is required with shared layers preserved where collaboration is wanted.
  • Separate tenants still have their place when parties share nothing; the two-layer model is the better answer for the far more common case where parties share most things and need to hide a slice.