Identity & tenancy
How real users, agent sessions, and tenant organizations stay separated — verified identity at ingress, authorization as derived facts, and fail-closed boundaries everywhere.
Tarski’s authorization model follows the platform’s one rule: session IDs are labels; ontology-derived authorization facts are permissions. Nothing is authorized because a server variable says so — access exists because observations derived it, which makes every grant and revocation replayable, auditable, and explainable.
Agent session isolation
Simultaneous users of the same deployed agent are separated by an authority tuple evaluated before every read and every write: agent, interface, trusted actor context, target identity, current evaluator facts, projection policy, and write-policy admission.
The evidence chain is ordinary Tarski derivation:
access.grant observation
→ mapper emits source.access_session_grant
→ ontology derives access.chat_session_visible(actor_id, session_id)
→ projections admit only visible rows
→ write policies admit only visible appends
Pause, end, or revoke evidence retracts visibility. Because observations are append-only, unauthorized messages are denied before append — leaked evidence can never need “fixing later.”
Two isolation patterns are supported: a per-session lineage (each session writes to its
own target, such as chat:{session_id}) and a shared lineage where projection policies
filter every row by actor and session. Physical separation is not the only mechanism — the
projection is the read boundary and the write policy is the append boundary.
Concretely: session catalogs omit unreadable sessions, hidden transcripts return empty,
hidden provenance seeds return projection.denied, model (LLM) context uses the same
projection and actor context and records an actor-context digest, and exports are
actor-scoped. Platform sign-in is not session access: cloud authentication only proves
you may reach the API; app-session access must exist as derived facts in the selected
target.
Verified end-user identity
Tarski is not an identity provider. Your app’s IdP keeps login UI, passwords, MFA, and directories. Tarski verifies the IdP’s tokens at ingress and turns them into trusted actor context:
IdP token → ingress verification → actor context (source=end_user_verified)
→ access.* facts → policy-gated appends → row-filtered projections
Verification terminates at ingress — local tarski serve and the hosted runtime verify
OIDC/JWT credentials before actor-context construction. Raw tokens and unverified claims
never reach mappers, helpers, evaluation, receipts, or exports.
Configuration is per app, per environment, in tarski.toml:
[end_user_identity.environments.local]
issuers = ["app_idp"] # issuer allow-list; labels namespace actor ids
audience = "my-app" # audience binding
algorithms = ["RS256"] # asymmetric only — HS*/none are unrepresentable
jwks = "config/idp-jwks.json" # pinned JWKS or issuer-pinned OIDC discovery
clock_skew_secs = 60 # explicit skew bound
Claim mapping is deterministic: actor_id is always user:{issuer_label}:{subject}.
Malformed configuration is a typed load-time failure — serve refuses to start rather than
run with verification silently disabled. Denials use typed end_user_identity.* codes
(token_malformed, issuer_undeclared, audience_mismatch, algorithm_rejected,
signature_invalid, credential_expired, and more), identical locally and hosted, and
never echo token material.
Two validity clocks
A request acts as an end user only while both hold:
- Credential validity — the token verifies right now (signature, issuer, audience, expiry within skew), statelessly on every request. There is no verify-once session bit.
- Access validity — the current worldview derives the required
access.*facts.
Expiry rejects at ingress even while access facts hold; a revocation observation collapses access at the next head even while the token is still valid. Replay never re-verifies tokens — historical admissions are explained by recorded observations and receipts.
The access.* account pattern
Observations like identity.account_created, identity.login_linked,
access.role_granted, and access.revoked derive account.identity_link (multiple
issuers can link to one account), account.role, account.revoked, and account.active.
session.account binds sessions to accounts — and a session claimed by more than one
account becomes session.contested and fails closed for everyone.
Get the whole pattern as a working start:
tarski scaffold --pattern end-user-auth
The scaffold ships identity config, the account ontology, a gated session lifecycle, a
row-filtered projection, and fixtures (including revoked-account and contested-session
paths) — green under tarski verify from the first run. The member-portal example app
shows the pattern end to end. From the TypeScript SDK, pass the user’s token per request
with the endUserToken option — see Client SDK.
tarski cloud deploy forwards only the selected environment’s identity block, and the
management plane configures end-user identity on the runtime before activation
promotes — a verified-end-user app can never go live with ingress silently unconfigured.
Platform-builder tiers
For platform builders whose customers are themselves tenant organizations, Tarski models four tiers — platform operator, platform customer, tenant organization, end user — as data on the one actor-context contract, not as parallel auth systems.
Delegation chains record ordered issuing-authority hops (at most two, strictly descending
tiers), and every violation fails closed before any context mints. Write policies,
projections, and session bindings can bind chain fields declaratively (actor.tier,
actor.issuing_authority.*, actor.delegation_chain.<tier_label>). Tenants on shared
runtime cells are isolated by an organization/tenant composite key, with per-tenant
concurrency ceilings returning typed tenant_backpressure denials while sibling tenants
keep admitting.
Managed Cloud tenant isolation
Two properties hold independently in Tarski Cloud:
- A caller can read or mutate only records authorized for its organization and token
scope. The session chooses the tenant, never the payload — an
organization_idin a request body cannot move a write into another tenant. - Records from different organizations can never collide in storage, even with identical project, app, environment, or secret names — record identity is organization-qualified and digest-suffixed.
Every management listing requires a valid, unexpired, non-revoked session, applies the same route-scope allowlist as mutations, and filters by the session’s organization. Unauthenticated or wrong-scope reads get typed 401/403 responses with no partial rows; membership-store unavailability fails sign-in closed; quota denials are typed and keep your prior scope selected. Selecting or publishing an existing workspace tuple is re-selection, not new workspace creation, so repeated verification never consumes extra quota.
For composing reads across tenant lineages — the multi-tenant read architecture — see Multi-tenant read models on the Cloud page.