Skip to content

Platform tenant API

The platform tenant API manages tenants without shell access to the API container: the same steps as ulams:tenant:create, set-env and delete (Tenants), behind /api/platform/*. The decision is ADR 0078, the details are in ADR 0085.

  • A user of the platform with the platform_admin permission (the platform’s admin role has it after AuthPermissionSeeder ran; run it once on an existing platform: php artisan db:seed --class=PermissionsSeeder).

  • A scoped token needs platform:read for reads and platform:write for changes, and lasts at most 30 days on a platform host (scoped tokens). Create one from a platform login:

    Terminal window
    printf '%s' "$PASSWORD" | ulams login --url http://api.localhost --email admin@ulams.app --password-stdin
    ulams tokens create --name platform-ci --scopes platform:write --expires-in-days 7 --json
  • Calls are throttled to 60 per minute per user (Rate limits) and, with a scoped token, recorded in the agent audit log.

Terminal window
ulams tenants list --fields slug,status,urls.front
ulams tenants get coffee
ulams tenants create acme --name "Acme Academy" --theme coffee --accent "#C2552D" --users 3 --demo # waits, prints steps
ulams tenants create acme --no-wait # returns tenant-op:<id>
ulams operations wait tenant-op:01j9z3k8m2x4q7r5t6v8w0y1ab
ulams tenants set-env acme --override AI_DRIVER=fake # values are never shown back
ulams tenants delete acme --yes # permanent: database, files, settings
ulams tenants operations list

A login on a platform host saves a platform profile. The commands exit 12 (FEATURE_DISABLED) with the variable to set when the host answers 404.

Send requests to the platform host (api.localhost in the supplied stack) with a platform token:

Terminal window
TOKEN=ulams_pat_... # platform:read or platform:write, see above
H=(-H "Authorization: Bearer $TOKEN" -H 'Accept: application/json')
curl -s "${H[@]}" http://api.localhost/api/platform/tenants
curl -s "${H[@]}" -H 'Content-Type: application/json' -H 'Idempotency-Key: acme-create-1' \
-X POST http://api.localhost/api/platform/tenants \
-d '{"slug":"acme","name":"Acme Academy","theme":"coffee","accent":"#C2552D","users":3,"demo":true}'
# 202 {"success":true,"message":"Tenant creation queued","data":{"operation":{"id":"01j9...","status":"queued",...},"tenant":{...}}}
curl -s "${H[@]}" http://api.localhost/api/platform/operations/<operation id> # poll until succeeded or failed
Request Does
GET /api/platform/tenants, GET /api/platform/tenants/{slug} List, show: status, URLs, finished steps, names of the setting overrides. Never a database password, key or override value.
POST /api/platform/tenants {slug, name?, theme?, accent?, users?, demo?} Queue creation. 202 with {operation, tenant}. 409 if the tenant is active or an operation for it runs; a failed tenant is resumed. 422 for an invalid slug.
PATCH /api/platform/tenants/{slug}/env {set?: {KEY: value}, unset?: [KEY]} Override or reset inheritable settings (ANTHROPIC_API_KEY, AI_DRIVER, AI_MODEL_*). 422 for any other key.
DELETE /api/platform/tenants/{slug} {confirm: "<slug>"} Queue deletion. 422 unless confirm equals the slug.
GET /api/platform/operations, GET /api/platform/operations/{id} Latest 50, or one: status (queued, running, succeeded, failed), steps with name, status, times and error, and error.

Provisioning runs as a queued job on the platform queue worker (the supplied stack has one) and takes about a minute: database, bucket, env, migrate, passport_keys, passport_client, permissions, lti_keys, demo. A step that failed is kept on the operation; asking for the tenant again continues from it. Deletion has the steps database, bucket, env, redis.

Status Cause
404 Not found. TENANCY_PLATFORM_API is not true, or the host is a tenant host (checked before authentication)
401 No or invalid token
403 The user lacks platform_admin, or the scoped token lacks platform:read / platform:write (scope_missing)
404 Tenant not found. Unknown slug on GET, PATCH or DELETE
409 operation_in_progress (body has data.operation) An operation for this tenant is queued or running
409 POST for a tenant that is already active
422 Invalid slug, name, theme (^[A-Za-z0-9_-]{1,40}$), accent (#RRGGBB) or users (0 to 50, default 5); an env key that is not inheritable; confirm is not the slug
429 More than 60 requests per minute