Skip to main content

Administration & Instance Settings

This page is for the people who run a tripl instance: managing members, granting the right level of access, issuing API keys for agents and scripts, and tuning the server-wide settings the app exposes in its UI.

tripl is a single multi-user instance ("workspace") that contains every project. Almost everything an administrator does lives under Settings, which has two contexts:

  • Workspace — Members, Data sources, API keys, your own Profile and Security, and the owner-only Instance sections.
  • Project — per-project configuration (General, Plan rules), covered in the user guide rather than here.
Settings vs. environment variables

The Instance sections in the UI let an owner override a subset of the server's configuration, stored in the instance database. Everything else — database/broker URLs, the encryption key, the application secret — is set only through environment variables. Sections differ in when an override applies — some at use-time, some on the next restart (see When changes take effect). For the full env-var reference (including the variables behind the settings below) see Configuration.

Roles & permissions

tripl has exactly three roles, defined server-side as the UserRole enum (owner, editor, viewer):

RoleCan readCan edit plan contentManage members & rolesInstance settings
ViewerYesNoNoNo
EditorYesYesNoNo
OwnerYesYesYesYes

How roles are assigned:

  • The first user to register becomes owner. This guarantees every instance has at least one operator who can manage roles. Every subsequent registration defaults to editor.
  • A viewer is never created automatically — an owner has to downgrade a member to viewer in Settings → Members.
  • Only an owner can change anyone's role. The API refuses to demote the last remaining owner (400 Cannot demote the last remaining owner), so you cannot lock yourself out of the instance.
Claiming a brand-new instance

tripl install provisions and starts a stack, but it stops at a running, empty instance — it does not create this first account, and no tool can. Registering over the API would mean putting a password on a command line, and the session cookie the reply sets is Secure, so it would be discarded on a plain-HTTP first run. So the last thing install prints is the instruction to open the URL in a browser and sign up; that first sign-up is the owner, and everything on this page assumes it has already happened. The provisioning side is Self-hosting & Deployment.

Role changes sign the user out

When an owner changes a member's role, every active session for that user is deleted in the same transaction, so the new permissions take effect on their next request. An in-flight request that already passed authentication still finishes with the old role; only the next request is affected.

How permissions are enforced

The backend gates endpoints with role/scope dependencies, not just UI hiding:

  • Editor-or-above is required for any mutation. Viewers receive 403 Editor role required.
  • Owner is required for member role changes and all Instance settings. Non-owners receive 403 Owner role required.
  • Owner-only endpoints additionally require an interactive owner session — an API key does not reach them, even a write-scoped key owned by an owner (403 Owner session required). One route is deliberately exempt: the metrics replay, which a write-scoped key backed by an owner may call.

Members

Settings → Members lists everyone with access to the workspace. The roster itself is visible to any authenticated user; only owners see the per-row role dropdown and can change roles. Non-owners see read-only role chips.

Each row shows the member's name (or email), email, join date, and role (Owner / Editor / Viewer). Owners cannot change their own role from this screen — use another owner account if you need to step down, and remember the last-owner guard above.

Registration ships open — close it once your team has accounts

Self-service registration used to be the only way to add a person, which is why it ships open by default. You can now invite people directly instead (see below), so closing registration no longer blocks onboarding. On an open instance anyone who can reach the URL can sign up, join as editor, and immediately read the whole tracking plan and this member roster — and edit any shared project. Data source connection details (host, port, username) are owner-only. Decide the policy before you expose the instance; see Security & access and Security & Hardening.

Invite a member

The recommended way to add someone, and the only one that works without opening the instance to the world:

  1. Settings → Members → Invite a member. Enter their email and pick a role.
  2. Copy the link it returns. It is shown once and cannot be retrieved afterwards — send it however you like (the instance may have no SMTP).
  3. They open the link, set a password, and land in the workspace at the role you chose.

Owner only, and only from a signed-in browser session — an API key cannot mint an account whatever its scope. The link works a single time, expires after 72 hours, and is bound to the address you typed, so it cannot be redeemed into a different identity. Pending invitations are listed under Settings → Members and revoking one kills its link immediately. Inviting the same address again invalidates the previous link.

This works while registration is Disabled — that is the point of it.

Adding a member while registration is Open: they can also just register themselves at the sign-in page, and you adjust their role from Settings → Members.

While registration is disabled the sign-in page shows no sign-up form at all, and POST /auth/register is refused with a 403 that tells the visitor to ask an owner. The one exception is an instance with no users at all — that first registration always works and becomes the owner, so a fresh or reset deploy can always be claimed. Registration is also rate-limited (see Security & access).

Profile & account security

These two sections live under Settings → Account and apply only to the signed-in user.

Profile

Settings → Profile shows your details pulled from the authenticated account:

  • Full name, Email, and Role — all read-only here. Role is "Set by a workspace owner."
Profile preferences are not persisted server-side

Avatar upload, the Preferences block (Timezone, Date format, Start of week) and the Notifications toggles (Incident alerts, Review requests, Weekly digest) are presentation-only in this release. They operate on local state and are not backed by an API endpoint, so they do not save across devices or sessions. Treat them as not-yet-wired.

Account security

Settings → Security presents Password change, Two-factor authentication, and Active-session management.

Account-security controls are not functional yet

The password-change form, 2FA toggle, recovery codes, and the "Sign out all" / active-sessions list are presentation-only placeholders — there are no backend endpoints behind them in the current release. The real authentication flows are register, login, logout, and the password reset pair (/auth/password-reset/request and /auth/password-reset/confirm) reached from the sign-in screen's Forgot your password? link — that link, not this form, is how a password is actually changed today. To invalidate a user's sessions today, change their role (which deletes their sessions) or have them log out. Sessions also expire automatically after the configured TTL (session_ttl_hours, default 168 hours / 7 days).

API keys & governance

API keys are long-lived bearer tokens for non-browser clients — LLM agents and CLI scripts. They are managed per user at Settings → API keys (backed by /api/v1/me/api-keys). A user only ever sees and revokes their own keys; there is no cross-user key administration, even for owners.

Creating a key

The create form (and the POST /api/v1/me/api-keys endpoint) takes:

FieldNotes
NameRequired, 1–100 chars. A human label (e.g. claude-agent).
Scoperead (default) or write. See below.
ProjectOptional. Bind the key to a single project by slug, or leave as All projects for full account reach.
Expires in (days)Optional, 1–3650. Blank means no expiration.

Scopes:

  • read — read/query operations only. Mutation surfaces return 403 API key has read-only scope; a few complex read-only queries use POST with a JSON body and remain available.
  • write — full editor-level access, still subject to the owning user's role. A write key is useless beyond reading if its owner is a viewer. Creating a write-scoped key itself requires the creator to be editor or above (a viewer cannot mint a write key, and the UI hides the option).

Project binding is orthogonal to scope. A project-bound key authenticates only /projects/{slug}/... routes for that one project; any other project (403 API key is not authorized for this project), or any instance-wide route (/me/..., /users, ...), is rejected (403 API key is scoped to a single project). Use it to fence an agent into a single project. Unbound keys keep full cross-project reach.

The token is shown exactly once

On creation, the full token is returned a single time and displayed in a "Copy your API key now" dialog. Copy it immediately — only a non-secret prefix (tk_<scope-letter>_<first chars>, e.g. tk_r_… / tk_w_…) is stored and shown afterward. Server-side, only a SHA-256 hash of the token is persisted, so a database dump cannot replay tokens.

Governing keys

  • Inventory. The All keys card lists every key with its name, prefix, scope chip (read/write), bound project (or "All projects"), and last-used / status (never used, used <date>, expired, or revoked). Its heading counts usable keys separately from dead ones — "7 active · 3 revoked or expired" — so the summary never files a revoked token as live. last_used_at is updated on use but throttled to at most once per ~60s to avoid write amplification on the auth hot path.
  • Revocation is immediate and soft. Revoking sets revoked_at; the key stops authenticating at once (callers begin receiving 401). Revoke is idempotent. Creating and revoking keys are recorded in the audit log (api_key.create / api_key.revoke).
  • Expiry is enforced server-side: an expired key authenticates as if unknown (401).

For how clients present these tokens (the Authorization: Bearer <token> header) and the endpoints they unlock, see the Agent API guide.

What a key cannot reach, whatever its scope

Owner-only endpoints require an interactive owner session, so an API key is 403 on them even when its owner is an owner. The single exception is the metrics replay: it only re-runs SQL an owner already authored, so an owner's tk_w_ key may trigger one. In practice that means connecting a data source and inviting a member are browser-only steps — no CLI, script or agent can do them for you, which is why tripl install hands you a URL at the end instead of finishing the job. The Operator CLI documents which of its commands need a key at all: install and upgrade need none, the diagnostics need tk_r_, and three verbs need tk_w_ behind an editor or owner.

Instance settings (owner only)

Settings → Instance is visible only to owners — non-owner accounts see an "Owner role is required to view or change instance-level settings" message, and the API rejects them (GET/PATCH/PUT /api/v1/settings all require an owner session). It exposes a curated subset of the server configuration as overrides stored in the database.

How overrides work

Each editable field resolves as database override → environment value. The defaults come from the environment / Settings object; saving a value in the UI writes an override row, and a per-section Reset removes the override so the field falls back to its env value. Most fields carry a small source badge with three states:

  • Override — a row exists in this instance's settings table. A section Reset clears it. An override whose value happens to equal the default still reads Override, because there is a row to clear.
  • Env — no override, and the value differs from the built-in default, so something delivered it: an environment variable or a .env line.
  • Default — the value equals the built-in default. That means either nothing was delivered for this setting, or what was delivered happens to match the default. From inside the process the two are indistinguishable, and a normalising validator can fold a delivered value onto the default the same way (LOG_LEVEL=info becomes INFO). The badge's tooltip says so.

The asymmetry is the point when you are verifying a deployment: a field badged Env is evidence that a variable reached the container. A field badged Default is not evidence that it did not.

Secrets are write-only

Secret fields — the AI API key, the search-embedding API key, and the SMTP password — are encrypted at rest (Fernet) and never returned to the browser. The UI never shows a stored secret's value: at most a Configured / Not configured placeholder and the same Override / Env / Default badge the other rows carry, which says where the secret came from and never what it is. Leave the field blank to keep the existing value, type a new value to replace it, or use Clear to remove the override. Encryption requires ENCRYPTION_KEY to be set (see Configuration).

When changes take effect

Sections apply at one of two times, and each section says which above its fields:

Use-time (no restart). The Runtime (query limits, app base URL), Email, and AI sections are resolved override → env value on each call, so edits apply immediately. The worker falls back to env-only config if it can't read the settings table, so background jobs never fail on a settings read.

Restart-time (next deploy). The Security & access (except Registration, which applies immediately), Storage, and Observability sections are consumed by parts of the server that are wired once at process start — the middleware stack (CORS, security headers, the session cookie), the auth rate limiters, the photo storage backend, logging, and the metrics route. At startup the saved overrides are applied onto the running configuration before any of those are built, so they take effect on the next restart/redeploy. You no longer need to also set the matching environment variable; the env var is just the default the override replaces.

Each section in the UI states its own answer rather than leaving you to work it out: Runtime, Email and AI say a saved override applies to the very next request or scan task, the next message tripl sends, and the next AI call respectively — no restart needed. Security & access, Storage and Observability say a saved override applies after the next restart (of the API, and of the workers too for Storage and Observability), with Security & access naming Self-service registration as the one field of its own that applies to the very next signup attempt. Every note also repeats that unset fields fall back to environment variables.

A running process won't pick these up live

Because these sections are read once at boot, editing them does not change a process that's already running — restart the API (and worker) container for the new values to take effect. A bad value (e.g. a CORS origin that locks you out) is recovered the same way you set it: edit it back, or clear the override, and restart. The matching environment variable still works as the baseline default — see Configuration.

The sections, with the fields each exposes:

Runtime

Core server configuration.

  • App base URL — used in emails, webhooks and the ingest endpoint (app_base_url). Like the rest of this section it is read per request, so correcting it here fixes those links immediately, with no restart. It does not reach API clients: /openapi.json publishes no servers block, so a generated client and the "Try it out" panel in /docs address whichever origin they fetched the spec from — see Base URL. Do check the value anyway: an address left over from setup is invisible in the UI and surfaces only as password-reset and alert links that open the wrong host (or nothing at all) for whoever clicks them.
  • Scan row limit default — default warehouse row cap for scans (scan_row_limit_default, default 50,000).
  • Metrics row limit default — default row cap for metric queries (metrics_row_limit_default, default 100,000).

Email

SMTP transport for alert delivery (and, in the UI's framing, invitations and digests). Leaving SMTP host blank disables email destinations.

  • SMTP host (smtp_host)
  • Port (smtp_port, default 587)
  • SMTP username (smtp_username)
  • SMTP password (smtp_password, secret — write-only)
  • Use TLS (smtp_use_tls, default on)
  • Default From address (smtp_from_address) — used when a destination doesn't override it.

AI

Powers anomaly explanations, schema/description suggestions, and the assistant. Disabled by default because plan content is sent to the configured provider when enabled.

  • Provider: AI enabled (ai_enabled), Base URL (ai_base_url, default https://api.openai.com/v1; http and localhost endpoints such as a local LLM are allowed), Model (ai_model, default gpt-4o-mini), AI API key (ai_api_key, secret), and a Test AI button that runs a live connection check against the provider.

  • Generation: Timeout seconds (ai_timeout_seconds, default 30), Max output tokens (ai_max_output_tokens, default 700), and three editable system prompts — Describe prompt, Ask prompt, Alert explanation prompt — which fall back to built-in defaults.

  • Search embeddings: toggle (search_embeddings_enabled), read-only Embeddings base URL (search_embedding_base_url, default https://api.openai.com/v1, env only), read-only Embedding dimensions (search_embedding_dimensions, default 1536, env only), provider (search_embedding_provider, default openai), model (search_embedding_model, default text-embedding-3-small), and Embedding API key (search_embedding_api_key, secret).

    The base URL is the endpoint every indexed event name, description and field value is POSTed to. It is shown precisely so an operator can confirm where that text is going without reading the source, which is why it carries a source badge like everything else: Env means something delivered SEARCH_EMBEDDING_BASE_URL to this container. It is reported, never accepted — the vectors already in the index were written against whatever endpoint produced them, and similarity across two embedding spaces is meaningless, so changing it is a re-index and a deploy rather than a setting.

If no key is set on either AI secret field, the server falls back to the OPENAI_API_KEY environment value when resolving the effective key.

Security & access

Authentication and network policy for everyone on the instance.

  • Registration (registration_mode, default Open) — whether strangers can create their own account. Open allows self-service signup: anyone who can reach this instance creates an account, joins as editor, and can immediately read the whole tracking plan and the member roster, and edit any shared project. Each data source's name, type and health are visible to everyone; its connection details (host, port, username, whether a password is set) are owner-only, and the password itself is never returned to anybody. Disabled refuses POST /auth/register with a 403 and hides the sign-up form on the sign-in page. It defaults to Open only because there is no invite or owner-creates-user flow yet, so a closed instance cannot onboard anyone; close it once your team has accounts. See Members for the onboarding flow.
  • Sessions: Session cookie name (session_cookie_name, default tripl_session), Session TTL hours (session_ttl_hours, default 168), Secure cookie (session_cookie_secure).
  • Network & headers: CORS allow origins (cors_allow_origins, comma- separated), Security headers (security_headers_enabled), HSTS (hsts_enabled) and HSTS max age (hsts_max_age_seconds), Content Security Policy (content_security_policy).
  • Rate limiting: master toggle (rate_limit_enabled), Login limit (rate_limit_login_per_minute, default 5/min), Register limit (rate_limit_register_per_hour, default 3/hour), and Trust X-Forwarded-For (rate_limit_trust_forwarded_for).
Registration is the one field here that applies immediately

Everything else in this section is read once at process start (see When changes take effect), but Registration is resolved on every signup attempt — closing the door must never wait for a redeploy. The REGISTRATION_MODE env var remains the default the override replaces.

Trust X-Forwarded-For only behind a trusted proxy

rate_limit_trust_forwarded_for defaults to false. Enable it only when a trusted proxy/load balancer sits in front and overwrites X-Real-IP on every request. On a directly-exposed API, a raw X-Forwarded-For is attacker-controlled — trusting it lets an unauthenticated caller rotate the header per request and bypass the rate limit entirely. Set it either way: the RATE_LIMIT_TRUST_FORWARDED_FOR env var and the Security & access override both work, and the override wins. Like the rest of this section it is read once at startup, so either route takes effect on the next restart.

Storage

Where event photos are persisted (photo_storage_backend: Local filesystem or Google Cloud Storage).

  • Backend: Photo storage backend, Photo max size (photo_max_size_mb, default 10), Allowed MIME types (photo_allowed_mime).
  • Local filesystem: Local photo directory (photo_local_dir, default ./var/photos).
  • Google Cloud Storage: GCS bucket (gcs_photo_bucket), GCS public URLs (gcs_photo_public), GCS credentials path (gcs_photo_credentials_path, blank falls back to Application Default Credentials), Signed URL TTL (gcs_photo_signed_url_ttl_seconds, default 3600).

Observability

How tripl reports its own health.

  • Logging: Log level (log_level: DEBUG/INFO/WARNING/ERROR/CRITICAL, default INFO), JSON logs (log_json), Request ID header (request_id_header, default X-Request-ID).
  • Metrics & tracing: Prometheus metrics (prometheus_metrics_enabled — exposes /metrics), OTLP endpoint (otel_exporter_otlp_endpoint — setting a non-empty value opts the API and worker into OpenTelemetry auto- instrumentation), OTEL service name (otel_service_name, default tripl).

System (read-only)

A health/build panel. Each tile shows Configured or Unset for: Debug mode, Database URL, Sync database URL, RabbitMQ URL, Redis URL, Encryption key, and the OpenAI fallback key. One further tile, Schema revision, reports the Alembic revision this instance's database is actually stamped with — the version_num in its alembic_version table — in one of four states:

  • the revision string, in a success tone, when it equals the migration head this build ships. The database is at the newest migration the running image knows about.
  • the revision string, in a danger tone, when it does not. The tile names the head this build ships, so you can see both revisions — but not which way round they are. Nothing here compares the applied revision against this build's migration graph, so this one state covers both causes described below: a migrate step that never ran here, and a rollback onto a database a newer release already upgraded.
  • the revision string, in a warning tone, when the database is stamped but this build's own migration head could not be determined. The revision is reported as read; only the comparison is missing.
  • Unknown, in a warning tone, when alembic_version itself could not be read: no such table, no row, or the database was unreachable. The tile still names the head when it has one. Unknown is reported as unknown rather than guessed at, so an instance whose revision merely could not be read is never painted as one whose migrations were skipped.

Nothing here is editable — it reflects the process's environment, plus this one value read live from the database each time the page loads — so you can confirm the instance is wired correctly and that its schema is where the build expects. The comparison is a plain equality, so any mismatch shows the same danger state with the build's head named beside it. The usual cause is a database that never had alembic upgrade head run against it. The other is a rollback: the migrate one-shot is forward-only and pulling an older image does not revert a schema change, so an older build against an already-upgraded database mismatches too — see Migrations are forward-only in the runbook.

  • Configuration — the full environment-variable reference behind these settings.
  • Security — hardening the instance (secrets, HTTPS, CORS).
  • Deployment — running the instance (compose / GHCR image) and what a restart picks up.
  • Operator CLItripl install / tripl upgrade for the host, and tripl doctor for checking the instance once it is up.
  • Agent API guide — using API keys from agents and scripts.
  • Troubleshooting — common operational issues.