Identity

Authentication

Trestle keeps dashboard administrators, application users and machine identities separate so one credential cannot silently acquire another audience's authority.

Application-user flow

  1. Read GET /api/v1/auth/capability to learn the registration flow (register, invite, request, or closed) your application should present. Trestle never delivers verification email, so capability reports emailDelivery: "manual".
  2. Register or sign in through the application authentication API.
  3. Receive a rotating refresh credential and short-lived access token.
  4. Send the access token as a bearer credential to collection, file and realtime APIs.
  5. When access expires, exchange the current refresh token once and persist its replacement atomically.
  6. On logout, revoke the refresh session and discard local credentials.

Discover the available sign-up path

Do not hard-code a registration form that may be disabled by the deployment. Read capability first and render the flow Trestle reports.

const response = await fetch(
  "https://api.example.com/api/v1/auth/capability",
  { headers: { Accept: "application/json" } },
);

if (!response.ok) throw new Error("Could not read auth capability");

const capability = await response.json();
showRegistrationFlow(capability.registration.flow);
showEmailDeliveryNotice(capability.registration.emailDelivery);

Log in from a browser

Application authentication uses JSON tokens, not the administrator dashboard cookie. Keep the short-lived access token in memory and send it only in the Authorization header.

const baseURL = location.origin;
let session = null;

async function login(email, password) {
  const response = await fetch(`${baseURL}/api/v1/auth/login`, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email, password }),
  });

  const body = await response.json();
  if (!response.ok) {
    throw new Error(body.error?.message ?? "Sign-in failed");
  }

  session = body;
  return body;
}

This example assumes your reverse proxy exposes the application and Trestle API on one origin. A successful response includes userId, email, accessToken, accessExpiresIn, refreshToken and expiresIn. Access tokens last 15 minutes. Refresh credentials last 30 days unless revoked, expired, replaced or disabled with their user.

Call an authenticated route

async function listMyIssues() {
  const response = await fetch(
    `${baseURL}/api/v1/collections/issues/records?limit=25`,
    {
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${session.accessToken}`,
      },
    },
  );

  const body = await response.json();
  if (!response.ok) {
    throw new Error(body.error?.message ?? "Could not load issues");
  }
  return body.items;
}

Trestle evaluates the collection's rules for the authenticated user. A token never bypasses row or field policy merely because the browser knows a record ID.

Rotate the refresh credential

A refresh token is single-use. Replace the complete stored session with the response from refresh before another request tries to use the old credential.

async function refreshSession() {
  const response = await fetch(`${baseURL}/api/v1/auth/refresh`, {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ refreshToken: session.refreshToken }),
  });

  const nextSession = await response.json();
  if (!response.ok) {
    session = null;
    throw new Error(nextSession.error?.message ?? "Session expired");
  }

  session = nextSession;
  return nextSession;
}

For a pure browser application, memory-only storage has the smallest XSS exposure but requires login after a page reload. If the session must survive reloads, persist the rotating refresh value in one IndexedDB transaction. A production application can instead keep it behind a small same-origin backend and issue its own HttpOnly, Secure, SameSite cookie. Avoid localStorage for long-lived credentials.

Log out

async function logout() {
  const refreshToken = session?.refreshToken;
  session = null;
  if (!refreshToken) return;

  await fetch(`${baseURL}/api/v1/auth/logout`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken }),
  });
}

Clear local state even when the network request fails. The server-side session may remain valid until it expires, so retry revocation when your application can do so safely.

Registration policies

Application-user registration is governed by one deployment-wide policy chosen by a Trestle administrator. open lets anyone register through POST /api/v1/auth/register. invite requires a valid invitation accepted at POST /api/v1/auth/invite/accept (body {"token","password"}; the bound email is taken from the invitation). approval accepts an access request (POST /api/v1/auth/access-request) that an administrator must approve before an activation link is issued. closed blocks all public registration; administrators provision accounts with single-use activation invitations. Under every policy administrators can provision users through activation invitations, and existing users continue to log in normally.

Public policy rejection uses stable machine codes: registration_closed, registration_invite_required, and registration_approval_required (all 403), with generic 409 registration_unavailable for a duplicate or otherwise unavailable registration, 400 invalid_invitation for any unusable invitation, and 503 registration_temporarily_unavailable when the registration lock cannot be acquired. The public responses never reveal whether an email already exists, whether a token is valid, or who is waiting for approval.

Invitation and activation tokens are returned to the administrator exactly once and are stored only as hashes. Because Trestle does not send email, the dashboard always offers the raw token to copy; when an activation base URL is configured it additionally renders a link with the token in the URL fragment, which the application reads client-side so the token never appears in HTTP request targets, proxy logs or referrer headers.

First-run choice

A new deployment selects its registration policy as part of first-run administrator setup and defaults the control to closed; an existing pre-v15 deployment upgrades with the historical open behavior preserved so nothing breaks. Policy changes apply immediately and are audited.

Administrator flow

The embedded dashboard uses a server-side cookie session, CSRF token and origin checks. Do not reuse that cookie from an application client or automation script. Use a personal token for administrator-owned automation.

Machine flow

Create a scoped service account for each deployed workload. Store the copy-once secret in that workload's secret manager and send it only over HTTPS. Never embed a service credential in public JavaScript, where every visitor can recover it.

Recovery boundary

Outbound email verification and password recovery are not yet implemented. Do not present a recovery UI that Trestle cannot complete.

Dogfood path

The Incident Desk keeps administrator sessions out of the application and uses short-lived application-user access tokens for every browser request. See the complete authentication flow.