Download all docs

Authentication & access

A circle is the subject of authentication: callers act as a circle, or as a member of one. Triform accepts bearer tokens (programmatic clients, agents) and cookie sessions (the browser UI).

The model

The circle is the subject

Authentication answers “which circle is this call acting as?” A request resolves to a single circle, and everything it touches — elements, data, the per-circle schema, the wallet (credentials, secrets, the AU balance; see billing-au) — is scoped to that circle. Members act on behalf of a circle rather than alongside it: one subject per request, never two.

Two vehicles, one identity

Every credential the platform accepts resolves to the same internal auth context — the handler that runs your call cannot tell, and must not care, which vehicle carried it. The middleware tries credential sources in order and stops at the first that parses:

  • triform_token cookie — the browser default. Set on login, sent automatically on every same-origin request.
  • Authorization: Bearer <jwt> — the programmatic default for CLI tools, agents, and backends.
  • trif_* API keys — minted tokens (below), presented in the same Authorization slot.
  • ?auth=<jwt> query parameter — for WebSocket upgrades and screencast surfaces that cannot attach a header. Short-lived tokens only: query strings leak into logs and history, so the middleware caps their remaining lifetime hard.

The two vehicles are not asymmetric in what they can do. There is no write operation that requires cookie auth over bearer — bearer requests are exempt from CSRF precisely because a browser never auto-attaches a cross-origin Authorization header. A call that “only works with cookies” is a client bug, not a server gate. The wire-level mechanics — the access/refresh token pair, ~1 h access lifetime, silent refresh, and the cookie-jar discipline a CLI needs to keep a session alive past an hour — are the reference’s job; see /docs/api/authentication.

Per-operation auth levels

Authorization is declared per operation, in each element’s ops.yaml, as an auth level on the operation. The runtime reads it from generated code, so the requirement is uniform across the HTTP API, the CLI, and agent tools. The common levels, from least to most privileged:

  • none — public; no credential required.
  • read — any authenticated member of the circle may call it.
  • write — mutating operations.
  • admin — circle-administrative operations (for example, every api-token operation is admin).

Some elements declare finer levels (execute, owner) where the operation warrants it. A call below the required level fails closed.

Minting scoped tokens — api-token

The api-token modifier mints bearer credentials for programmatic access. generate returns a trif_-prefixed token once in the response — the server keeps only a SHA-256 hash and the 12-character prefix for identification, so a lost token is reissued, never recovered. Tokens carry scopes (default ["read"]), an optional expiry, and a type (personal, service, ci). rotate issues a replacement with an optional grace_period_hours window for the old one; revoke invalidates immediately and irreversibly.

api-token is a settings modifier: it stores credentials but does not cascade onto other elements or intercept requests. It is the thing you authenticate with — not the thing that decides whether a given call is allowed.

Gating access — auth-policy

The auth-policy modifier is the gate. Attached to an element, it runs as request middleware (early in the chain) and decides whether a call may proceed: it returns 401 when authentication is required but absent, 403 when a credential is present but insufficient. Its spec declares the required authentication.methods (default bearer) and an authorization.type (default rbac). Cascade is restrictive — when an inherited policy and a local one disagree, the stricter wins, so tightening a parent can never be silently loosened by a child. The common mistake is forgetting to attach one to a public-facing element.

So the division of labour is: api-token makes the key, auth-policy guards the door, and the per-op auth level sets the baseline every operation requires regardless.

Dev-auth bypass (local only)

Debug builds expose POST /api/auth/dev with { "circle_name": "..." }, which logs you in as that circle and auto-creates it if absent — the fast path for local development against triform.dev. It is compiled out of release builds entirely.

Related

  • Reference: /docs/api/authentication — the canonical how-to (headers, flows)
  • Concept: tenancy-airtight — the circle as subject
  • Concept: billing-au — the wallet behind a circle (credentials, AU balance)
  • Element: api-token, auth-policy, oauth, rate-limit