ObjectStackObjectStack

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

PropertyTypeRequiredDescription
idstring✅User ID
namestringoptionalDisplay name
emailstringoptionalEmail address
positionsstring[]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
isPlatformAdminbooleanoptionalThe 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.
organizationIdstring | nulloptionalActive organization ID (null = platform/unscoped)

On this page