Eval User schema — Identity Protocol reference
EvalUser — the one user-context contract. Reference for EvalUser: every property with its type and default.
EvalUser — the one user-context contract (ADR-0068 D1).
The signed-in user exposed to every predicate surface (server formula, server
RLS, client UI gates) under the canonical variable name current_user
(aliases user, ctx.user) with an identical shape. A predicate such as
current_user.positions.exists(p, p == 'org_admin') (or
'org_admin' in current_user.positions) therefore evaluates identically wherever
it is written.
positions: string[] is the only canonical membership field (renamed from
roles, ADR-0090 D3). A singular field is NOT part of this contract — its legacy "overwritten to 'admin' on promotion"
behavior is the footgun this eliminates.
Which axis positions is — and which it is not (#15136)
It is the security axis: the built-in identity names plus the
position names the principal actually holds through sys_user_position
(ADR-0057 D4) and their active membership, plus the everyone anchor
(ADR-0090 D5) — exactly the set /auth/me/permissions reports and
resolveUserAuthzGrants resolves. Every surface derives it from that one
authority, so the "identical shape" promise above is a property of the
producer, not a convention producers are asked to honour.
⛔ It is not the better-auth sys_user.role scalar. That scalar is an
authentication-layer field on a table this platform does not own; it stays
published, unchanged, as user.role (the one exception ADR-0090 D3's word
ban carves out), and a consumer that genuinely wants it reads it there.
⚠️ The session payload used to union the scalar into this array and omit the
sys_user_position names entirely, which made a position-narrowed client
gate answer FALSE for its own holder — silently, since the root and the key
were both bound and CEL raised nothing. The documented org_admin example
kept working throughout because that name sits on both axes. If you are
adding a producer of this shape: derive it from the authority, never
assemble it from whatever identity fields are in reach.
See also: docs/adr/0068-unified-user-context-and-built-in-identity-roles.md
Source: packages/spec/src/identity/eval-user.zod.ts
TypeScript Usage
import { EvalUserSchema } from '@objectstack/spec/identity';
import type { EvalUser } from '@objectstack/spec/identity';
// Validate data
const result = EvalUserSchema.parse(data);EvalUser
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | User ID |
| name | string | optional | Display name |
string | optional | Email address | |
| positions | string[] | optional (default: []) | Canonical position/identity names the user holds — built-in identity names plus the positions assigned through sys_user_position, scope-resolved (ADR-0068 D3). The security axis, the same set /auth/me/permissions reports; NOT the better-auth user.role scalar, which remains published as user.role |
| isPlatformAdmin | boolean | optional | The PLATFORM_ADMIN standing of ADR-0095 D3: true when the platform resolves the user, per request, to the PLATFORM_ADMIN posture rung — from the deployment's declared administrator list (OS_PLATFORM_OWNER_EMAIL) under every tenancy posture, or from an unscoped admin_full_access grant under the single posture. The predicate platform-operator gates read (current_user.isPlatformAdmin == true, ADR-0068 D4). The resolver projects 'platform_admin' into positions from the same grant, so the two agree for every genuine administrator; gate on this key, never on the positions array. |
| organizationId | string | null | optional | Active organization ID (null = platform/unscoped) |
Identity Protocol
The ObjectStack Identity Protocol in 5 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example.
Identity
Defines the standard user, account, and session data models for ObjectStack. These schemas represent "who is logged in" and their associated data.