Skip to main content
Turn on the request-auth gate and write a jq access-control policy. Access control explains the model this configures. The runtime gates its HTTP routes behind API keys and per-user policies. A request presents an sk-… key; the access-control middleware resolves the key’s user and evaluates that user’s jq policy against the request context. You provision keys, map routes to scopes, and attach jq conditions.

Prerequisites

The gate is on by default. Before it can admit requests in a real deployment, these backing pieces must be in place.
1

Access control is enabled

The gate runs unless you set ACCESS_CONTROL_ENABLE=false. Leave it unset (or true) in production — the quickstart disables it only for local, auth-free development.
2

A Redis for the live auth context

The identity provider’s api-key records and the per-user live counters live in a plain Redis — any standard redis-server. No RedisJSON module or redis-stack image is required. There is no baked-in default: point the connection with ACCESS_CONTROL_REDIS_URL, or let it resolve from the shared TAI_DEFAULT_REDIS_URL — with the gate on, one must be set.
3

A Postgres policy store

Scopes, route mappings, and per-user policy bodies live in Postgres — the only policy store. It lives in the named database the core skeleton component binds to — the default database unless TAI_DB_BINDING_SKELETON points it elsewhere. Configure that database, including its password via TAI_DATABASE_DEFAULT_PG_PASSWORD.
4

An identity provider in the manifest

The skeleton ships no concrete identity provider, so a deployment with the gate on must name one as a lifecycle module in the manifest. The default is the redis-backed api-key provider:
examples/access_control/identity_manifest.yaml
It registers itself as the redis provider at import; ACCESS_CONTROL_AUTH_PROVIDERS (an ordered JSON list, default ["redis"]) names the chain the verifier tries. Swap the module and the provider name to use a different authn backend.
5

Probe routes are public by default

The guard denies any route it has no mapping for to ordinary identities (a condition-free * super-admin key is admitted — a root identity is never gated by a missing route row). The liveness and readiness probes carry no key, but they are served public by the app’s own route-level declaration: /health and /ready ship in ACCESS_CONTROL_ACKNOWLEDGED_PUBLIC_ROUTES, so an orchestrator reaches them with no key and no manual pin.

Mint the first admin key

With the gate on and no key yet, there is no authenticated door to create one — tai keys create itself needs a key. The one-shot tai keys bootstrap door (POST /api/keys/bootstrap) closes that gap: it mints the first condition-free ["*"] admin key, gated by a boot-time token.
The gate resolves its token in one of two ways:
  • ACCESS_CONTROL_BOOTSTRAP_TOKEN — an operator-set token. When set, it is the token the door expects.
  • the auto-generated token — absent an operator token, each deployment fixes one random token once at startup and logs it a single time (first-key bootstrap token: …). Only someone who can read the server log can mint the first key.
Pass the token out of band, never on the command line — --token - reads it as one line from stdin (a value on argv leaks via ps and shell history). The door is one-shot and secure by default: it refuses with 409 Already initialized the moment any key exists, a wrong or absent token is a generic 403 (with a per-IP backoff that escalates against a token flood), and with the gate off (ACCESS_CONTROL_ENABLE=false) it is disabled with a 501 — there is no key to protect. For local, auth-free development, ACCESS_CONTROL_BOOTSTRAP_OPEN=true opens the door without a token. Once the first admin key is in hand, every further key is created through the authenticated surface below.

Provision an API key

Create a key for a user. The raw sk-… value is printed once — capture it then. Grant scopes with the repeatable --scope, and attach an inline jq authorization condition with --condition.
The scopes are granted with the repeatable --scope flag — the command’s own help confirms it:
examples/cli/keys_create_help.sh
The CLI itself authenticates with a key. Pass it out of band — read one line from stdin with the global --api-key-stdin flag, or set TAI_API_KEY in the environment. There is no --api-key VALUE flag, because a value would leak via ps and shell history.

Map routes to scopes

A scope maps URLs to a named policy target. Add a URL to a scope, optionally with a dynamic match pattern, and list the current mappings.
Scope a pattern to a surface; never blanket the whole site. Every API route already declares its own protection — a scope row or pattern adds scoping, it is not what keeps /api private. Deny wins: a path that matches both a protected pattern and a public mapping resolves protected, so a whole-site pattern like ^/.*$ also matches browser page navigations and blocks the dataless Studio shell — a deep-link refresh then meets a 401 JSON body instead of the app shell and its login flow, and the shell exposes no data anyway. Tighten a real surface with a scoped pattern (^/api/tools/.*), never a blanket. The plugin studio-asset door stays public through the always_public_route_patterns setting, which resolves it public alone, ahead of any route row (a plugin bundle loads as a native ESM import that cannot send an auth header); protect it by editing that setting, not with a scope row.

Write and validate a jq condition

A jq condition compiles against the request context and returns whether the request is allowed. Validate one before you save it — compile it, and optionally sample-evaluate it against a JqAuthContext-shaped sample.
Attach the validated condition when you create or edit a key with --condition.
1

Draft the condition

Write a jq expression over the request context and compile-check it with tai keys validate-condition.
2

Attach it to a key

Create the key with --condition — a templated-text JSON object carrying either inline jq (content) or a stored condition (id), plus optional render kwargs.
3

Track policy changes

Each user’s enforced policy is append-only and versioned. List the history with tai keys policy-versions, and roll back with tai keys policy-rollback.
A key is revoked immediately with tai keys delete — the next request with it fails to authenticate. Rotate a key by revoking and re-issuing; editing a key never rotates its material.

Verify enforcement

Prove the gate before you trust it: call a guarded route with two keys. A key whose policy grants the route’s scope reaches the resource with 200; a valid key that lacks the scope is denied with a fixed 403. Set TAI_BASE_URL to your server and the X-Api-Key header to each key. An allowed key reaches the resource:
examples/access_control/verify_allowed.sh
A valid but under-scoped key is denied — a fixed 403, with the full reason logged only server-side:
examples/access_control/verify_denied.sh
Both requests run against the real AuthAdapter → AccessControlAuthBackend → ResourceGuardMiddleware chain in docs CI, so a change that breaks allow/deny enforcement breaks this step.

Delegate with owned keys

A key minted by a non-admin is an owned key — capped at a subset of its owner’s scopes, isolated to its own data, and attenuated with the owner on every request. To mint a capped key for a teammate or device, inspect what it can do with tai auth whoami, and share it by one-time QR claim link, follow the owned-keys guide.

See also