Changelog
Operations

Auth & API keys

The server-side API key that authorizes everything, the optional read token that lets browsers read streams directly, and why authorization is yours to own.

teaspill authenticates every request at the gateway, and it gives you two credentials to do it with. Pick by where the caller runs.

  • API keys are the primary path: a server-side secret that authorizes everything — spawn, send, control, registration, reads. Put them in backends.
  • Read tokens are optional and narrow: short-lived tokens that let a browser read streams and catalog data directly, and nothing else.

API keys

An API key is the credential your backend, your UIs' server, and the CLI present to the gateway. Every route except the health check requires one, as Authorization: Bearer <key>. A key looks like tsp_…, is a 256-bit random value, and is all-or-nothing: there is no per-key scoping at the platform layer.

Mint keys with the teaspill keys command. It talks to the catalog database directly, so it needs a database connection rather than a gateway URL:

export DATABASE_URL='postgresql://teaspill:teaspill@localhost:5432/teaspill?sslmode=disable'

teaspill keys create --label my-backend   # prints the tsp_ token ONCE
teaspill keys ls                           # id, status, created_at, label — never the token
teaspill keys revoke <id | id-prefix | tsp_token>

create prints the plaintext token exactly once; the database stores only its sha256 hash, so the token is never recoverable — capture it now or mint a new one. revoke is a soft delete the gateway rejects on immediately; it accepts a full key id, an id prefix, or the plaintext token. Pass --json to any subcommand for machine-readable output, or --database-url instead of the environment variable.

keys is the one CLI command that needs DATABASE_URL rather than --gateway. If you run it from outside the Compose network, point it at the host-published Postgres port as shown above.

The bootstrap key

GATEWAY_BOOTSTRAP_API_KEY is a literal key the gateway accepts without any database row, so a fresh stack is usable before you have minted anything — the reference deployment uses it to register on first boot.

The bootstrap key is a dev convenience only. It bypasses the database entirely, so it cannot be revoked without restarting the gateway. Mint real keys with teaspill keys create and remove GATEWAY_BOOTSTRAP_API_KEY before production.

Minting programmatically

The same primitives are available in code, for a provisioning script or an admin panel of your own:

import { createApiKey } from "@teaspill/catalog"; // mints the token + stores its hash

createApiKey returns the plaintext token once, exactly like the CLI, and persists only the hash.

Read tokens

By default, browser reads go through your backend: your server holds the API key and proxies stream requests. That is fine, but it puts your backend in the path of the chattiest traffic. The read token removes it: a short-lived token, minted by your server, that lets a browser read /streams/* and /shapes/* directly from the gateway.

Enable the path by setting GATEWAY_JWT_SECRET on the gateway. With no secret set, the path is off and only API keys are accepted. Then mint tokens server-side:

import { mintReadToken } from "@teaspill/agents-sdk";

const jwt = await mintReadToken({
  // one entity prefix covers both its timeline and its deltas
  pfx: "/streams/t/default/agents/researcher/01j.../",
  ttlSeconds: 300, // keep it short — reconnecting is cheap
  secret: process.env.GATEWAY_JWT_SECRET!,
});
// hand `jwt` to the browser; it sends `Authorization: Bearer <jwt>`

The token carries a single path-prefix claim, pfx. The gateway authorizes a request only if the requested path starts with that prefix, so one token scopes a browser to exactly one entity's streams.

Mint the prefix with a trailing slash. /streams/t/default/agents/researcher/01j.../ matches that entity's streams but not a sibling whose id merely starts the same way. Without the slash, the prefix leaks across neighboring entities.

Keep TTLs short. When a token expires, the gateway returns 401 with a body telling the client to reconnect with a fresh one — cheap, because streams are resumable: the browser resumes from its last offset. Expiry is checked with a small clock-skew leeway so a token that tips over mid-request is not rejected spuriously.

A read token is reads-only, by construction. It is honored only on GET requests to the two read route families; on any write route or non-GET method it is not even considered. A read token can never spawn, send, or control an agent — those always require an API key, so writes never bypass your backend.

CORS for browser reads

Because browsers read cross-origin, the gateway answers CORS preflight and sets response headers for GET on /streams/* and /shapes/* only — never for the write routes. The default allowed origin is *, which is safe here: the read token, not the origin, gates the actual read. Pin it by setting GATEWAY_CORS_ALLOW_ORIGINS to a comma-separated list.

You own authorization

teaspill has no permissions model. It answers one question — is this a valid credential? — and never is this caller allowed to do this particular thing? Deciding which user may spawn which agent, or read which conversation, is your backend's job: hold the API key server-side, apply your own authorization, and mint a narrowly-scoped read token only for the streams a given user is allowed to see. This is a deliberate stance, not a gap — a platform-level permissions model would be a second, weaker copy of the one your application already has.

Environment reference

VariableDefaultMeaning
DATABASE_URLPostgres connection for API keys. Required unless the bootstrap key is set.
GATEWAY_BOOTSTRAP_API_KEYDev-only literal key accepted without a database row.
GATEWAY_JWT_SECRETShared secret enabling the read-token path. Unset means the path is off.
GATEWAY_JWT_CLOCK_TOLERANCE_SECONDS60Clock-skew leeway when checking a read token's expiry.
GATEWAY_CORS_ALLOW_ORIGINS*Allowed origins for GET reads of /streams/* and /shapes/*.

Next steps

The read routes these credentials protect are specified in the Gateway API reference; using read tokens from a UI is walked through in Frontend integration.