AuralisDocs
OperationsWorkspace settings

Save first-run setup progress

Record where a workspace has got to in first-run setup.

/v1/tenants/me/onboardingEditor
Operation id
patch_my_tenant_onboarding
Access
A viewer is refused.
PATCH/v1/tenants/me/onboarding

Record where a workspace has got to in first-run setup.

This is the setup wizard's own save. It writes the onboarding blob, whose shape is the client's, and it touches nothing else in settings.

deployment_mode can be chosen here once. After it has been set, this route will not change it again, and an attempt to clear it is refused rather than quietly accepted. Changing it later is an admin operation on PATCH /v1/tenants/me.

Two callers finishing setup at the same moment cannot both pass the one-time gate. One wins and the other is refused.

The response is the workspace in the same shape the read returns.

First-run setup, at the editor floor rather than admin.

PATCH /tenants/me is deliberately admin-only, because it carries provider API keys, spend caps, name and data region, and this route must not become a second way to reach any of that. It only ever touches two paths inside settings, onboarding and, once, deployment_mode, and does so with jsonb_set on those paths specifically, never by reading the whole settings blob into Python and writing it back whole. A blob rewrite would let an editor's onboarding save silently clobber whatever an admin committed to a sibling key (daily_usd_cap, default_model, ...) in the gap between this route's read and its write, measured happening 8/25 rounds under concurrency in review. jsonb_set never touches a key it was not told to.

Every Keycloak-backed member lands as editor (the realm has no admin concept), so gating the wizard's own "finish setup" action at admin means no SSO user can ever complete or skip it: both the completion and the skip button 403 identically, which is indistinguishable from the product being broken.

Two properties make the deployment_mode one-time gate hold under adversarial and concurrent use, not just the single well-behaved request:

  • SELECT ... FOR UPDATE takes a row lock for the rest of this transaction. Two concurrent completions on a fresh tenant now serialize on that lock rather than both reading "not complete" before either commits (review reproduced 15/15 double-crossings without it).
  • completed_at is the gate's own input, so this route never lets the caller clear it. An explicit null or falsy value is refused outright, so a caller cannot re-open the gate to walk back through it, and once the stored row already carries a completed_at, an incoming one is dropped rather than merged, so a resend after success is a harmless no-op instead of a second bite at deployment_mode. Review's two-request bypass ({"completed_at": null} then complete-again-with-a-new-mode) depended on this route accepting the first request; it no longer does.

Tenancy: the request is scoped to one workspace, and the credential presented is what selects it. A caller with no membership in that workspace is refused with 404, which is indistinguishable from a workspace that does not exist.

Authorisation: editor, admin or owner. A viewer receives 403 insufficient_role.

Access: Authorization: Bearer carrying either a realm access token or an API key. A key must hold the tenants:write scope, and the workspace role of the person who owns it must meet the floor above.

Authorization

AuthorizationBearer <token>

A realm-issued access token, presented as Authorization: Bearer <token>. It acts as the person who signed in, and every operation resolves their workspace role live from the membership record.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

curl -X PATCH "https://example.com/v1/tenants/me/onboarding" \  -H "Content-Type: application/json" \  -d '{    "onboarding": {}  }'
{}