---
id: "auth/mental-model"
title: "Mental Model"
---

# Mental Model

> **Design RFC**
>
> This page describes the target authorisation model. Omnitron today ships only the base auth surface; the role/permission/condition/RLS layering shown below is a design reference.

```mermaid
flowchart TB
  Token["Verified JWT: sub, roles, permissions, pv, sid, cnf.fp"]
  Counter[("Permission version in Redis")]
  Invocation["Netron invocation wrapper"]
  Context["Admitted AuthContext"]
  Token --> Invocation
  Counter -->|Check permission version| Invocation
  Invocation -->|Admit request| Context
  Context --> Gate["Authorization gate"]
  Context --> RLS["Kysera row-level policies"]
  Context --> Policy["Organization policy"]
  Context --> Service["Service permission checks"]
  Gate --> Audit["Privileged-action audit"]
  RLS --> Audit
  Policy --> Audit
  Service --> Audit
```

Four layers. They share **one** identity (the JWT-resolved
`AuthContext`), **one** permission matcher (`permissionMatches` /
`hasPermission` in `netron/auth/utils.ts`), **one** counter
(`perm-v:{userId}`), and **one** audit sink (`audit_logs`).

## Layer 1 — Roles

A user has one or more roles. Each role names a default
permission set. Roles are coarse — fewer than ten platform-
wide and a handful per organisation. Tier predicates
(`canModerateUserWithRole`) live in `@yourorg/auth-utils/role-hierarchy`
and are the only consumer of role-priority numbers; gates that
care about specific capabilities check **permissions**, not
roles.

## Layer 2 — Permissions

Strings in the canonical registry, matched by `permissionMatches`,
baked into the JWT at mint time. The `.*` wildcard expands to the
set of keys under its prefix (a bare prefix without the star is
*not* a hierarchical grant — it matches only the exact key). The
matcher is the **single source of truth** for "does this granted
set authorise this required permission?" — used unchanged on both
the platform and org scopes, on the server `@Auth` decorator,
on the @kysera/rls policy ctx, and on the frontend
`<PermissionGuard>` / `usePermission()`.

## Layer 3 — ABAC conditions

When role + permission isn't enough, a `PolicyDefinition`
attaches an attribute test: time window, recent step-up MFA,
request IP, feature flag, rate-limit. Built-in factories cover
the common shapes; custom policies are plain objects registered
on the `PolicyEngine`. Composition via `{and|or|not}`.

## Layer 4 — RLS

@kysera/rls policies fire on every DB query and consume the
SAME `AuthContext` that the application-side gates see. The
STRICT tier (`audit_logs`, `platform_roles`, `users`,
`user_sessions`, `organization_roles`) fails closed on a
missing auth context — raw SQL without context cannot leak
through. Other tables run permissive for anonymous reads.

## Live propagation

Every mutation to a user's effective permission set bumps
`perm-v:{userId}`. The invocation wrapper compares the
JWT's `pv` snapshot against the live counter on every call;
mismatch ⇒ refresh-token flow ⇒ new JWT with the updated set.
The propagation window is bounded by one RPC round-trip —
no re-sign-in.

## Why one mental model?

Before the unification:

- The platform-scope matcher (`shared/permissions.ts`) accepted
  only `.*` wildcards.
- The org-scope matcher (`dynamic-policy.service.ts`) walked
  segment wildcards AND honoured hierarchical prefix grant.
- Two registries (`shared/permissions.ts` + org-roles JSON)
  could disagree.
- Three frontend copies of `ADMIN_ROLES = ['admin','superadmin']`
  drifted.
- `audit_logs` had a pass-through RLS policy.
- The `/admin` route gated on a hand-coded role-set that
  silently excluded moderator and security tiers.

Each of those was a real bug. The architecture above closes
them by making the layers share substrate — there is exactly
one place to look when something authorises (or doesn't)
unexpectedly.

## See also

- [Permissions](/docs/auth/permissions) — grammar, matcher, registry.
- [Per-user overrides](/docs/auth/overrides) — additive grants and
  live propagation.
- [ABAC conditions](/docs/auth/abac-conditions) — non-permission gates.
- [RLS bridge](/docs/auth/rls-bridge) — DB-layer enforcement +
  permVer wiring.
- [Audit trail](/docs/auth/audit-trail) — accountability layer.