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.
/v1/tenants/me/onboardingRecord 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 UPDATEtakes 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_atis 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 acompleted_at, an incoming one is dropped rather than merged, so a resend after success is a harmless no-op instead of a second bite atdeployment_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.
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": {} }'{}