Visibility and Privacy
Visibility rules control what a query or mutation actually returns once permission to call it has already been granted (see Permission Model Reference). They apply to the GraphQL types those operations return (CargoListing, Conversation, Shipment) and answer two separate questions:
- Which instances of this type can this user see at all?
- Of the fields on a visible instance, which ones does this particular user see?
These two questions are independent. A user might be allowed to see a given CargoListing exists, but only some of its fields, or might not be able to see it at all, in which case field visibility never even comes up.
Which Instances Are Visible
Every type has a scope that determines the widest audience it can ever reach:
| Scope | Meaning | Who Sees It | Example |
|---|---|---|---|
own_only |
Only the creator can see it | Just the owner | A draft conversation only its creator can see, even if others are eventually assigned |
assigned |
Only users (or groups) in assigned_to can see it |
Anyone in the instance's assigned_to list, directly or via a group |
A conversation only its participants can see |
public |
Anyone can see it, once published | Everyone, once the instance is published | A cargo listing, visible to all capacity providers once published |
all |
Anyone can see it, regardless of assignment or state | Admin only, in practice | Admins reviewing all shipments |
A type's scope isn't necessarily fixed forever. See Scope Changes With State below, where a CargoListing moves from own_only to public to assigned over its lifetime.
Regardless of scope, two viewers always see an instance: its owner (@owner, whoever created it) and admins. Scope governs everyone else.
Scopes can also carry an extra condition: for example, assigned scope limited to conversations that are still active or pending, so archived conversations drop out of view even though the user is still technically assigned.
Special Symbols
Assignment lists (assigned_to) and scope conditions are written in terms of three symbols:
| Symbol | Meaning | Example |
|---|---|---|
@self |
The current user | Matches if the current user is in an instance's assigned_to list |
@group |
Any of the current user's groups | Matches if any of the user's groups appear in assigned_to |
@owner |
The instance's owner | Always true for whoever created/owns the instance |
assigned_to, and therefore @group, is also the only way a group affects visibility: a group doesn't carry its own visibility rule. It only gains visibility into a specific instance by being added to that instance's assigned_to list. See Sharing & Resource Assignment for how that list gets populated, and Assignment & Visibility for the group case specifically.
Which Fields Are Visible
Once an instance is visible at all, field visibility narrows it further. A permission specifies which fields are allowed for each operation. Only listed fields are visible. If a field is not listed, it's hidden.
Grouped by viewer: a list of allowed fields per relationship to the instance:
| Viewer | Allowed Fields |
|---|---|
@owner |
["*"] (all fields) |
@assigned |
["id", "status", "location", "eta"] (specific fields only) |
public |
["title", "origin", "destination", "price"] (public fields) |
admin |
["*"] (all fields, including hidden ones) |
Example configuration:
cargoListing:
owner: ["*"] # sees every field
assigned: ["id", "status", "location", "eta"] # tracking only
public: ["title", "origin", "destination", "price"] # public info
admin: ["*"] # everything
Grouped by field: the same rule, read from the field's side: on a CargoListing, title and origin might be listed for the public viewer, while price and internalNotes are listed only for @owner and admin.
Either way, the outcome is identical: a non-owner, non-admin user sees only the fields listed for them: price and internalNotes are silently omitted from the response, never an error, just absent.
Default Rules by Type
| Type | Owner Sees | Assigned Party Sees | Public Sees | Admin Sees |
|---|---|---|---|---|
| CargoListing | Everything, including internal notes | -- (not an assignment-scoped type) | Public fields of published listings only (title, origin, destination, weight, volume, dates) | Everything |
| Conversation | -- (assignment-scoped, not owner-based) | Everything, once assigned | Not visible at all unless assigned | Everything, on every conversation |
| Shipment | Everything, including cost breakdown | Tracking fields only (id, status, location, eta, progress); cost breakdown and internal notes hidden | Not visible | Everything |
Reading this table alongside the scope model above: CargoListing's scope is public (reachable by everyone once published), but the owner column exists because the owner relationship always overrides scope, per Which Instances Are Visible. Conversation's scope is assigned. There's no public column with real content because its scope never reaches that far.
Scope Changes With State
A type's scope isn't always static. It can change as the instance moves through its lifecycle. CargoListing, for example:
| State | Scope | Fields |
|---|---|---|
| Draft | own_only |
Everything |
| Published | public |
Public fields only (title, origin, destination, price); internal notes hidden |
| Matched | assigned |
Everything, for those assigned |
| Shipped | assigned |
Everything, for those assigned |
See State-Based Constraints for how the same state also locks which mutations are callable at all, independent of visibility.
Enforcement Order
Visibility is checked after the permission check for the operation itself (see GraphQL Implementation for that full request flow), in two steps:
- Instance visibility: is this specific instance visible to this user at all? If not, it's excluded, from a single lookup that means a 404 (existence isn't leaked), from a list query it's just dropped from the results.
- Field visibility: of the fields the operation is otherwise permitted to return, which does this instance's current rule allow for this user? Hidden fields are silently omitted, never an error, and never a per-field 403, since checking every field on every request that way would add cost the app doesn't need at scale.
This ensures no data leaks, users only see what they should, and admins can override any visibility rule.