thygon

Identity & Access Control

Let users and organizations authenticate, manage identities, and enforce permissions.

Authorization at Scale

Authorization at scale means millions of actors, resources, and relationships, deep hierarchies such as folders within folders, and decisions needed on nearly every request within a few milliseconds. Those decisions are also made in many places at once: the public API, service backends, background workers, webhooks, and admin tools all need the same answer, at an acceptable cost.

Where Time Goes

Rendering diagram…

Diagram source
flowchart LR
  check["check(user, edit, doc)"] --> cache{Decision cache}
  cache -->|hit| result[Decision]
  cache -->|miss| engine[Policy engine]
  engine --> walk[Walk relationships: doc → folder → folder → team → user]
  walk --> store[(Relationship store)]
  engine --> result

The cost of a decision grows with the depth of the relationships it has to follow. A document nested ten folders deep, shared through nested groups, can turn one check into dozens of lookups.

Techniques

TechniqueEffectTrade-off
Decision cachingRepeated checks return in microseconds.Stale decisions until invalidation; see Revocation & Consistency.
Flattened group membershipNested groups resolved ahead of time into direct memberships.Recomputation when group structure changes.
Purpose-built enginesRelationship engines such as SpiceDB or OpenFGA, designed for graph-shaped checks.Another datastore to run and keep consistent.
Batch checksMany decisions in one call, for pages that show many resources.Requires engine and API support.
Engine placementEngine as a sidecar or library next to the service, avoiding a network hop.Policy and data must be distributed to every replica.

Distributed Enforcement

Every entry point must enforce access, and each check it makes adds latency. Splitting the work by layer keeps checks cheap where they can be and precise where they must be.

Rendering diagram…

Diagram source
flowchart LR
  web[Web app] --> gw[API gateway]
  mobile[Mobile app] --> gw
  partners[API clients] --> gw
  gw -->|authenticate, coarse checks| svc1[Projects service]
  gw --> svc2[Billing service]
  queue[(Job queue)] --> worker[Workers]
  hooks[Inbound webhooks] --> svc2
  admin[Admin tools] --> svc1

  svc1 -->|fine-grained check| engine[Policy engine]
  svc2 -->|fine-grained check| engine
  worker -->|fine-grained check| engine
  admin -->|fine-grained check| engine
LayerEnforcesDoes not replace
Gateway or edgeAuthentication, token validity, scopes, rate limitsResource-level decisions, which need product data.
ServiceResource-level authorization through the policy engineData-layer protection against bugs in the service.
Data layerOrganization isolation, for example row-level securityBusiness rules that depend on more than the tenant.

Each entry point accepts its own kind of credential: session cookies, bearer tokens, API keys, OAuth tokens, or service certificates. Authentication normalizes all of them into one principal, so authorization code never inspects a credential and every service decides from the same structure:

{
  "type": "user",
  "id": "usr_…",
  "organizationId": "org_…",
  "scopes": ["projects:read"],
  "authMethod": "api_key",
  "actingFor": null,
  "authenticatedAt": "2026-11-02T14:03:11Z"
}

Design Points

  • Measure the shape of the data. Hierarchy depth, group nesting, and the fan-out of shares predict performance better than raw counts.
  • Bound the model. Limit nesting depth and the number of paths the engine may follow; unbounded models eventually produce unbounded latency.
  • Load test with realistic data. Generate data with the depth and skew of production, including the largest customer, and test with cold and warm caches.
  • Deny by default in the framework. Routes and resolvers without an explicit authorization declaration fail to register or return 403. Making a route public is a deliberate, reviewed choice.
  • One enforcement library. Every service uses the same client for principals and checks, so behavior, caching, and logging are identical everywhere.
  • Route inventory. A generated list of every endpoint with its authentication and authorization requirement, checked in CI, makes gaps visible in review.
  • Workers and webhooks carry identity. Jobs record the principal that created them and re-check permissions when they run. Inbound webhooks verify signatures and map to a specific integration principal.

Watch For

  • Hot resources. A resource shared with the whole organization is checked constantly; make sure its decisions are cached and its invalidation does not cascade.
  • Per-request fan-out. A list page that checks each item individually multiplies latency by the page size. That is the problem described in Authorization-Aware Data Access.
  • GraphQL and batch endpoints. One request can touch many resource types; authorization belongs on each field or resolver, not only on the top-level query.
  • Internal tools. Admin dashboards are often the least protected and most powerful entry point.
  • Inconsistent results. The same action allowed in the API but denied in the UI, or the reverse, signals duplicated rules. Move them into the policy engine.

Implementation

Implement this subsystem

Implementation starts with technical scoping. We review your existing architecture, agree on boundaries, interfaces, and acceptance criteria, and adapt this design to fit before committing to delivery milestones.

There is no off-the-shelf package or fixed price: scope, timeline, and cost follow from that alignment.

Implement this subsystem (opens in a new tab)