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.

  1. What belongs here, and what doesn’t
  2. Endpoints
  3. The PlatformSettings object
  4. GET /platform/settings
  5. PATCH /platform/settings
  6. Read-only maintenance
  7. 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_open and announcement, 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_mode is ignored.
  • operations.view: everything, including read_only and updated_at. This read always comes straight from the database, so the console has a current ETag to 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-Match is required. Without it, the request is 400 if_match_required. A stale one is 409 version_conflict, as everywhere else. These are the switches most worth not overwriting by accident.
  • signup_open and read_only are booleans, and null is 422.
  • client_flags is merged one level deep, like Settings notifications (Conventions → Partial updates). Each flag you send replaces that flag, a flag sent as null is 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, is 422 validation_failed.
  • announcement is replaced whole, and null clears it.
  • A field this object doesn’t have is 422 validation_failed (undeclared_key), so a typo like readonly can’t be silently ignored. updated_at is 422 read_only_field.
  • Every change writes Audit Event platform.settings_updated (target type platform_settings, target id live or test), with signup_open, read_only, client_flags, and announcement in before and after. A PATCH that changes nothing writes no event.
  • The instance that handled the PATCH drops 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 superadmin can always lift a freeze.

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

Everything else is refused, including signup, email verification, password reset, accepting invitations, and SCIM writes. Scheduled jobs and Stripe 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).