GraphQL Implementation
This section covers, at a conceptual level, how permission checks, visibility rules, and resource assignment are enforced for GraphQL requests. It describes what happens and why, not how it's coded. The actual schema (types, directives, resolvers) is defined separately.
Overview
Permission enforcement happens on every GraphQL request, in order:
flowchart TD
A[User Query] --> B[GraphQL Request]
B --> C{Authenticated?}
C -->|No| X1[Reject]
C -->|Yes| D{Permitted to call this operation?}
D -->|No| X2[403 Error]
D -->|Yes| E{Object visible to user?}
E -->|No| X3[404 Not Found]
E -->|Yes| F[Execute Operation]
F --> G[Filter Hidden Fields, silently]
G --> H[Return Response]
- 403 means the user's groups don't grant this operation at all.
- 404 means the operation ran, but the specific instance isn't visible to this user (
own_only/assigned/publicscope, see Visibility & Privacy). Existence isn't leaked to users who can't see it. - Field filtering never errors. A hidden field is simply omitted from the response, whether it's hidden by the requesting operation's permitted
outfields or by the returned type's own field-visibility rules. See Enforcement Order.
Schema Permission Registry
Why a Registry
Permissions reference operations by name (cargoListing, createCargoListing, ...). That list needs a single source of truth. Rather than maintaining a separate table of valid operation names by hand, it's derived directly from the GraphQL schema at startup: every query and mutation the schema actually exposes is collected into a registry automatically.
Simple to maintain
Adding a new permission-checked operation is the only step required: define it in the schema and ship it. The registry picks it up automatically the next time the app starts. There's no separate registry to hand-edit, and nothing that can quietly drift out of sync with the deployed schema.
How It Works
Because the registry lives in the same process as the GraphQL server, it doesn't need a separate build step, migration, or network call. On startup, the app walks the schema it has already loaded and records every query and mutation name it finds, along with their available fields. The result is a small in-memory lookup that answers two questions: which operations exist, and which fields belong to a given operation's input and output.
Group permissions are validated against this lookup instead of trusting free-text operation names. That means a permission referencing an operation the schema doesn't actually expose fails immediately when the group is created or updated, rather than silently matching nothing later when a user calls it.
Exposing It to the Admin UI
Groups Management (the admin panel) needs to show valid operations and fields in its dropdowns without loading the entire GraphQL schema client-side. Instead of maintaining that list by hand, the app writes a read-only snapshot of the in-memory registry to the database each time it starts up. That snapshot is only ever written by the app itself, on boot, never edited directly, which is what keeps an admin from being able to define a group permission against an operation or field that doesn't actually exist in the schema.
Request Context
Every GraphQL request carries the authenticated user's identity and their resolved groups, established once at the top of the request (via the existing Google Auth / Okta login flow, see Signup Flow) and available throughout permission and visibility checks for that request.
Permission Checks
A permission check answers one question: does any of the user's groups (including inherited ones) grant this operation? If the operation carries constraints (e.g. a state condition), those are evaluated against the specific instance being acted on. See Permission Evaluation for the full walkthrough of how effective permissions are computed across multiple groups.
Visibility & Field Filtering
Once an operation is permitted, two more checks apply to the type instance(s) involved:
- Instance visibility: can this user see this particular instance at all, based on its scope (
own_only,assigned,public,all)? See Which Instances Are Visible. - Field visibility: of the fields the operation is otherwise permitted to return, which ones does this instance's current visibility rule allow for this user? See Which Fields Are Visible.
Both checks run after the permission check and before the response is returned, and both filter silently rather than erroring.
Assignment
Assignment (who's in an instance's assigned_to list) is what makes assigned and public visibility scopes resolve to specific instances for a specific user, and is also the sole way a group influences visibility. See Sharing & Resource Assignment and Assignment & Visibility.
Error Handling
Two error shapes cover all denial cases:
- Permission denied (403): the user's groups don't grant this operation.
- Not found (404): used both when the instance genuinely doesn't exist and when it exists but isn't visible to this user, so existence isn't leaked either way.
There is no third error shape for hidden fields. Those are always omitted silently, never rejected.