Appearance
The administration site
The studio's Admin area, at /admin, is where administrators manage users, roles, API keys, sessions and the licence, read the audit log and usage, empty the trash, change the settings and the branding, and turn font sources on.
Getting there
Administration is a workspace in the studio's sidebar (Alt+3, or Go to administration in the command palette), and Go to administration is in the account group of the designer's More menu. It shows to anyone who may see one of its pages. /admin opens the first page you may see, and the sidebar lists the others.
| Page | Path | What it does | Needs |
|---|---|---|---|
| Users | /admin/users | Search, create, change roles, disable, set a temporary password, reset two-step sign-in, unlink an identity, sign sessions out, delete | user.manage |
| Roles | /admin/roles | The built-in roles; create, edit and delete custom roles; who holds each; each SSO provider's role rules | user.manage |
| API keys | /admin/keys | Create (the secret is shown once), rotate, change rate limits, revoke | user.manage |
| Sessions | /admin/sessions | Every signed-in session, by user, and signing one out | user.manage |
| Licence | /admin/licence | The Licence dialog's content as a page: install, remove, features, limits and their use | user.manage |
| Audit | /admin/audit | The audit log, newest first, filtered by time, who, action, template and environment; export to CSV or JSON lines | audit.read |
| Usage | /admin/usage | Renders, pages, jobs, requests, 429s and errors per day, by template, environment or format, with render times; the records of the last days; CSV exports | audit.read |
| Trash | /admin/trash | The catalog's Trash view: restore, delete for good, empty | template.read; purging trash.purge |
| Settings | /admin/settings | Every setting with what it does and its origin, searchable; change them, and reportd restarts once on them all (settings.md) | settings.manage |
| Branding | /admin/branding | The product name, colours, logo, favicon and sign-in page, with a preview (branding.md) | settings.manage, and the branding licence feature |
| Font sources | /admin/font-sources | The sources library authors import fonts from: the Google Fonts and Fontsource presets and declared sources, on or off, Test, and a form per kind (fonts.md) | fonts.sources |
A page you may not see, opened by its URL, goes back to /admin. reportd checks every permission itself: hiding a page is only a convenience, and every route below answers 403 without its permission.
Users
The list is searched by name or email (an identity's or an invite's) and comes a page at a time. Each user shows their roles per environment, how they sign in, whether two-step sign-in is on, when they last signed in, and whether they are disabled or have a temporary password. Roles a provider's rules gave are shown apart: only the provider changes those. The designer seats used are shown against the licence's (licence.md).
- New user makes a password user, with a temporary password they change at their first sign-in, or invites one to sign in through a provider with their email (sso.md).
- Manage opens a user: their roles (a row per role and environment, "all environments" or one), Disable or Enable, a temporary password, Reset two-step sign-in (mfa.md), unlinking an identity, their sessions, and Delete.
- The last administrator stays. reportd refuses a change that would leave no enabled user with
user.manage, and the page shows the refusal as it comes. You can't disable or delete yourself here. - Disabling a user, setting their password or resetting their two-step sign-in signs them out everywhere.
Roles
There are seven built-in roles, which don't change: administrator, designer, author, publisher, viewer, reader and service. A custom role is any set of the permissions.
- Create and Edit choose its permissions. Whoever holds the role has the new ones from their next request.
- Delete works only for a role no one holds, granted or mapped from a provider. A role still held is refused, and the page names the users and keys that hold it.
- An edit that would leave no one with
user.manageis refused, and so is one that would make designers past the licence's seats. - Single sign-on role rules shows each OIDC provider's claim and SAML provider's attribute, its rules and its default, and the users with mapped roles. The rules are config (
auth.oidc.providers[].roles,auth.saml.providers[].roles, sso.md), so the page shows them and doesn't change them.
settings.manage is the permission for settings and branding. Only the administrator role has it, and an agent key never does, whatever its roles, as with trash.purge. fonts.sources, for the font sources, is the same: a custom role can be given it, and an agent key never has it. So is templates.sources, for the template sources designers copy new reports from (template-sources.md).
API keys
Each key shows its owner (an agent key's user), roles, expiry, when it was last used and its own rate limits. Agent keys are marked.
- New key makes a service's key, or an agent's acting for a user, who caps what it may do. Its secret is shown once, with a copy button: reportd keeps only its hash.
- Rotate issues a new secret, as a new key with the same name, kind, user, roles, rate limits and expiry. The old one keeps working for an overlap, 7 days unless you choose 0 to 90, or until its own expiry if that is sooner, so the new one can be put in place first. Both are listed meanwhile, the old one marked Rotated.
- Limits sets the key's own rates per class (rate-limits.md); Revoke stops it at once.
- A key's last use is written at most once a minute, so a busy key doesn't write on every request.
Sessions
Every signed-in session across users, the most recently used first, filtered by user: where (browser and address), how it signed in, when it was last used and when it ends. Sign out ends one on every node. Your own is ended by signing out. It is each user's list from Manage, gathered in one place (state.md).
The audit log
Every change, render, promotion, sign-in and sign-out is an event in the audit log: who did it (a user, a key, an agent and the user it acts for, or reportd itself), through what (the API, MCP, the assistant, a command, a sweep), the action, and the template and environment it touched. The Audit page lists them newest first, 100 at a time with Show more, and filters them by a time range, who (an agent's user matches too), the start of the action (template. finds template.create, template.trash and the rest), a template and an environment. Export CSV and Export JSON lines save every event the filters select, newest first; in the CSV a cell that a spreadsheet would read as a formula starts with '. Each export is itself audited, as audit.export.
- Where it is:
dataDir/audit.log, one JSON object a line. Ataudit.maxSizeMB(64 by default) it is renamed todataDir/audit/audit-<time>.log, a new one begins, and the old one is gzipped to.log.gzin the background (audit text compresses to about a tenth).<time>is when it rotated, in RFC 3339's basic form (20261024T101500.000Z), since Windows refuses colons in names. Reading, exporting and the licence clock (licence.md) span the rotated files. - How long: rotated files are kept for ever by default. Every render is audited, so a busy install of 100,000 renders a day writes about 25 MB a day, roughly 9 GB a year, or about 1 GB gzipped. With
audit.retentionDaysset, the daily sweep (the trash's, at start and every 24 hours) removes the rotated files older than that, and on the shared state the table's events too. The live file is never removed. - On the shared state (
state.driver: postgres, state.md): each event also goes to theaudit_eventstable, and the page and the API read that, so they show every node's events. Each event names the node that wrote it (the host's name). Each node'saudit.logstays as its own copy. When Postgres can't be reached, the node keeps its events (up to 10,000) and sends them once it can; its file has them all the same.reportd state importfills the table from the node's files, the rotated ones too.
yaml
audit:
maxSizeMB: 64 # rotate the live file at this size
retentionDays: 0 # 0 keeps the rotated files (and the table's events) for everUsage
reportd counts what the install does, in two tiers:
- Records, kept for
usage.detailDays(7 by default): one for each render (who, template, environment, format, where from, the outcome, pages, time taken and bytes), job (its runs, outcome and time), webhook delivery (its outcome and attempts), sign-in (the method and outcome; a failed password sign-in records the client's address, never the name typed), 429 and 5xx answer (the rate-limit class, and who or the address). API requests are too many to record one by one: each node writes one tally a minute for each rate-limit class (rate-limits.md). - Daily totals, kept for
usage.retentionDays(0 by default: for ever): once a UTC day ends, its records are rolled up into renders by template, environment, format and outcome, with pages, bytes and the render time as total, median (p50), p95 and maximum; the most renders running at once, in all and on each node; jobs and webhook deliveries by outcome; requests, 429s and 5xx answers by class; and sign-ins by method and outcome.
A render's outcome is ok, error (the engine, or anything after it, failed) or refused (turned down before the engine: a permission, the data's size, a template in the trash). Render times are of the renders that reached the engine. Its source is api (an API key), studio (a signed-in user), viewer, job, mcp or assistant.
The Usage page totals a range of UTC days, the last 30 by default: tiles for the renders, pages, render times, peak concurrent renders, jobs and callbacks, API requests with 429s and server errors, and sign-ins; a bar for each day of the range, the failed and refused on top; the renders grouped by day, template, environment or format, which a template or an environment narrows; and the requests by class. Today, which isn't rolled up yet, is totalled from its records and marked so far. Totals grouped over several days or groups take their median and p95 from a histogram, so they are within 5%; one day's, or one group's, are exact. Below, the records of the last days drill down by kind, who and template, with the slowest renders. Export totals (CSV) and Export records (CSV) save what is shown.
- Where it is: each node buffers its records and writes them every 5 seconds and when it stops: to
dataDir/usage/records/<yyyy-mm-dd>.jsonl, one file a UTC day, and the totals todataDir/usage/daily/<yyyy-mm>.json, replaced whole. On the shared state they go to theusage_recordsandusage_dailytables, and one node at a time rolls up, under a Postgres advisory lock. - The rollup runs at start, for any day missed while reportd was stopped, and every hour. It works a day out from all of its records and replaces what was there, so running it twice changes nothing. A day rolled up within 10 minutes of its end is rolled up once more, for a node that wrote late. Records are removed once their day is older than
usage.detailDays, always after it is rolled up. - Size: about 150 bytes a render, so about 15 MB a day at 100,000 renders, and about 100 MB for 7 days. A day's totals are about 120 KB for a busy install with 200 templates, about 45 MB a year.
- Not counted: the assistant's model tokens, which wait for the token-usage item, and Prometheus-style metrics, which are the observability item's. Usage is for people.
yaml
usage:
detailDays: 7 # how long the records are kept
retentionDays: 0 # 0 keeps the daily totals for everThe API behind it
| Route | What |
|---|---|
GET /v1/users?q=&cursor=&limit= | Users by name, those q matches, a page at a time (100 by default, at most 500); next is the next page's cursor. Each has lastSignIn. |
PUT /v1/roles/{name} | Replaces a custom role's permissions. 400 built-in-role; 409 when no one would be left with user.manage, or licence-limit for the designer seats. |
DELETE /v1/roles/{name} | Deletes a custom role. 409 role-in-use with users and keys when it is held. |
GET /v1/roles/mappings | Each SSO provider's role rules, as config has them. |
POST /v1/keys/{id}/rotate | {overlapDays} (0 to 90, default 7); answers {key, record, previous}. The new record's rotatedFrom names the old key. |
GET /v1/sessions?user= | Every user's sessions, or one user's, each with its username, named by id and never by token. |
GET /v1/audit?from=&to=&principal=&action=&template=&environment=&cursor=&limit= | A page of events, newest first (200 by default, at most 1000), and next, the next page's cursor. from and to take an RFC 3339 time or a date (to's date includes its day); action is a prefix. 400 bad-cursor for a cursor it didn't hand out. |
GET /v1/audit/export?format= | Every event the same filters select, as a download: jsonl (the default) or csv. Audited as audit.export. |
GET /v1/usage?from=&to=&by=&template=&environment= | The days' totals, grouped by day (the default), template, environment or format: rows, the summary of them all, and each of the days, a day not rolled up yet marked partial. from and to are dates; format=csv answers the rows. |
GET /v1/usage/records?from=&to=&kind=&template=&principal=&cursor=&limit= | A page of records, newest first; with format=csv, every one the filters select. |
The keys' list carries each key's lastUsed. The audit and usage routes need audit.read, the rest user.manage; the API reference has them in full. They work the same on both state drivers (state.md): roles, keys and the last sign-in and use are kept with the users, in auth.json or the shared Postgres schema.
What is audited
Besides the users' and keys' events there were, these are audited: role.create <name>, role.change <name>, role.delete <name>, key.rotate <old id> <new id>, and audit.export.