Platform settings
A small set of runtime switches an operator can change without a deploy: closing signup, freezing writes during an incident or a migration, an announcement banner, and the feature flags that Hub and other first-party clients read.
- What belongs here, and what doesn’t
- Endpoints
- The PlatformSettings object
GET /platform/settingsPATCH /platform/settings- Read-only maintenance
- Errors specific to this resource
What belongs here, and what doesn’t
Platform settings hold operational switches and client flags only. Security-critical values (token lifetimes, the password policy, CORS origins, alarm thresholds) stay in code and Terraform on purpose, where every change is reviewed before it ships. Nothing here can weaken authentication or authorization.
Each mode has its own settings. A test-mode credential reads and changes test mode’s row, so you can rehearse a freeze or try a flag without touching live traffic.
Endpoints
| Method | Path | Requires | Purpose |
|---|---|---|---|
GET |
/platform/settings |
none; more fields with a token, and with operations.view |
Read the settings that apply to the caller. |
PATCH |
/platform/settings |
operations.manage, and If-Match |
Change them. |
The PlatformSettings object
// GET /platform/settings, as an operations.view caller
{
"signup_open": true,
"announcement": { "text": "Scheduled maintenance Saturday 02:00–03:00 UTC.", "level": "info" },
"client_flags": { "sso": false, "passkeys": true, "organizations": true, "billing_upgrade_ios": false },
"read_only": false,
"updated_at": "2026-10-27T16:00:00Z"
}
| Field | Who sees it | Meaning |
|---|---|---|
signup_open |
everyone | false closes POST /auth/signup with 403 signup_closed. Invitations still work. Default true. |
announcement |
everyone | A banner for clients to show, or null. text is 1–500 characters; level is info, warning, or critical. |
client_flags |
any authenticated caller | Remote-configurable client feature flags: flag name to boolean. Names match ^[a-z][a-z0-9_]{0,63}$, with at most 100 flags. The platform doesn’t interpret them; clients do. A flag that’s absent is up to the client, so clients should pick a safe default. |
read_only |
operations.view |
true puts the platform in read-only maintenance. Default false. |
updated_at |
operations.view |
When the settings last changed. |
Every response carries an ETag, the row’s version.
GET /platform/settings
Authentication is optional. What comes back depends on the caller:
- No token:
signup_openandannouncement, for live mode, or for test mode with?test_mode=true. A sign-up screen can check whether signup is open before it shows the form. - Any token (a User’s, or an API Key’s): the caller’s own mode, plus
client_flags.test_modeis ignored. operations.view: everything, includingread_onlyandupdated_at. This read always comes straight from the database, so the console has a currentETagto send back.
Everyone else may see a copy up to 30 seconds old. Each API instance caches each mode’s settings for 30 seconds, so a change takes effect everywhere within 30 seconds.
An invalid token is 401, not a fallback to the unauthenticated view.
PATCH /platform/settings
// Request (If-Match: W/"12")
{ "read_only": true, "client_flags": { "billing": false, "sso": null },
"announcement": { "text": "We're upgrading the database. Changes are paused for a few minutes.", "level": "warning" } }
Requires operations.manage, held by superadmin. It’s platform-only, so an app-scoped API Key can never hold it. The response is 200 with the full object, as an operations.view caller sees it.
If-Matchis required. Without it, the request is400 if_match_required. A stale one is409 version_conflict, as everywhere else. These are the switches most worth not overwriting by accident.signup_openandread_onlyare booleans, andnullis422.client_flagsis merged one level deep, like Settingsnotifications(Conventions → Partial updates). Each flag you send replaces that flag, a flag sent asnullis removed, and flags you don’t send are untouched. A name that doesn’t match the pattern, or more than 100 flags after the merge, is422 validation_failed.announcementis replaced whole, andnullclears it.- A field this object doesn’t have is
422 validation_failed(undeclared_key), so a typo likereadonlycan’t be silently ignored.updated_atis422 read_only_field. - Every change writes Audit Event
platform.settings_updated(target typeplatform_settings, target idliveortest), withsignup_open,read_only,client_flags, andannouncementinbeforeandafter. APATCHthat changes nothing writes no event. - The instance that handled the
PATCHdrops its cached copy at once. Other instances pick up the change within 30 seconds. - It stays callable during read-only maintenance, so an operator who isn’t
superadmincan always lift a freeze. - It’s a step-up operation. A User session needs an MFA check in the last 10 minutes, or it gets
403 reauthentication_required.
Read-only maintenance
While read_only is true for a mode, every request in that mode other than a GET returns 503 service_unavailable with Retry-After: 300 and details.reason: "read_only", unless the caller is a User holding the superadmin Role. It’s the same answer a write to a Tenant mid-migration gets, so clients that already back off and retry on it need nothing new. API Keys never bypass it, even with every permission.
These stay open, because a freeze pauses changes; it doesn’t take the platform down:
- every
GET - signing in, staying signed in, and ending sessions:
POST /auth/login,/auth/mfa/verify,/auth/token/refresh,/auth/logout, SSO callbacks, and revoking sessions (DELETE /users/{id}/sessions/{sessionId},POST /users/{id}/sessions/revoke-all), which an incident may need - the tokens a signed-in User needs to keep using their Applications:
POST /auth/app-tokens,/auth/oauth/authorization-codes, and/auth/oauth/token PATCH /platform/settings, so the freeze can be lifted, withPOST /auth/reauthenticatefor its step-up, and TOTP enrollment (POST /users/{id}/mfa/totpand/confirm) or passkey registration (POST /users/{id}/mfa/passkeys/registration-optionsand/users/{id}/mfa/passkeys) for an operator held by staff MFA; and the passkey options for sign-in and re-authentication (POST /auth/mfa/passkey-options,/auth/reauthenticate/passkey-options)
Everything else is refused, including signup, email verification, password reset, accepting invitations, and SCIM writes. Scheduled jobs and payment processor webhooks aren’t requests to this API and keep running, so a subscription that renews during a freeze is still recorded.
Errors specific to this resource
| Code | Status | When |
|---|---|---|
if_match_required |
400 | PATCH /platform/settings without an If-Match header. |
signup_closed |
403 | POST /auth/signup while signup_open is false for that mode. |
service_unavailable |
503 | A write during read-only maintenance (details.reason: "read_only", Retry-After: 300). |