Accounts and apps

Everything you deploy belongs to an app, and every app belongs to an account.

Docs menu

Sign up to create an account for your team, then create an app for each project, such as a blog, a staging copy, or a customer's instance.

Accounts

An account has:

  • Members, each with a role (admin or member). account.member.set adds a member or changes their role, and account.member.remove removes one. account.invite.create returns an invite link once. account.invite.accept joins the signed-in person to the account named by the invite.
  • Keys, scoped to the whole account (see Keys below).
  • Apps, each with a slug, data, functions, site, and keys.

account.create makes an account owned by the signed-in person. account.get reads one; account.list lists the accounts the caller belongs to. account.update renames an account.

Apps (tenants)

Operation names use the word tenant for an app (tenant.*). Each app has a database, functions, and site isolated from other apps. An app is identified by its slug, the name in its host (<slug>.<root>) and in routes such as GET /v1/_tenants/{slug}/limits for tenant.limits.

account.app.create creates an app for an account and returns its key once. The account's plan sets the number of apps it may have (max_apps in the plans file; see Plans and limits). A call above that limit returns 409 PLAN_LIMIT with field: "apps", max_apps, and apps. A plan that allows more apps raises the limit. An account-level max_apps, set with the system key, takes precedence over the plan's value. account.app.list lists an account's apps, and account.app.delete deletes an app and its data. tenant.usage.self reads an app's storage, documents, request rate, and function limits.

When the account has more apps than its plan allows

An account sets the order in which its apps run. If the plan allows fewer apps than the account has, the first max_apps apps in that order keep running and the rest are paused. This can happen when a subscription ends or is not renewed, or when the account moves to a smaller plan. A paused app retains its data, files, keys, functions, versions, and schedules. Its storage still counts toward the account's space. Requests and function calls return 403 APP_OVER_PLAN. Scheduled runs are skipped until the app runs again and are logged as suspended.

The order starts as creation order, oldest first, and a new app joins at the end. A paused app runs again, at once and with nothing lost, when any of these happens:

  • the account moves it into the first max_apps (account.apps.order),
  • an app ahead of it is deleted,
  • the plan allows more apps.

An app suspended for another reason stays suspended when apps are reordered or the plan changes. Its requests and function calls return 403 TENANT_SUSPENDED. The response does not disclose the reason. Each refused route call counts in usage as calls.refused.suspended.

account.apps.order (PUT /v1/_accounts/{id}/apps/order) accepts calls from the account's owners and admins or an account key. Its apps argument must contain every app id from account.app.list exactly once, in the order the apps should run. The response lists each app in order with active, which indicates whether it is within the first max_apps. It also returns max_apps, where 0 means no limit.

account.app.list returns each app's position in the order (starting at 0), active, and paused (paused because of the plan), along with the account's max_apps. In the console, you can order apps on the account's Apps page by dragging them or using the move buttons. A line follows the first max_apps apps, and each app below it is marked "Paused: over your plan's N apps". Each change is recorded in the account audit trail as account.apps.order. account.get returns the max_apps in force: the account-level value, the plan's value if none is set on the account, or the server setting if neither is set. A value of 0 means no limit.

{"name": "substrio_account_app_list", "arguments": {"id": 1}}
# {"apps":[{"id":7,"slug":"blog","position":0,"active":true,"paused":false,...},
#          {"id":9,"slug":"shop","position":1,"active":false,"paused":true,...}],"max_apps":1,"next_cursor":null}
{"name": "substrio_account_apps_order", "arguments": {"id": 1, "apps": [9, 7]}}
# {"apps":[{"id":9,"slug":"shop","active":true},{"id":7,"slug":"blog","active":false}],"max_apps":1}

shop now runs and blog is paused. Calls to a paused app get:

{"error":"This app is paused: ...","code":"APP_OVER_PLAN","status":403,"next_action":"reduce_request_or_increase_limit",
 "detail":"This app is paused: the account's plan allows 1 app. Move it into the first 1 on the account's Apps page, or upgrade.",
 "max_apps":1}
Code Status When
APP_OVER_PLAN 403 any request or function call to an app past the first max_apps of its account's order. detail names the limit and max_apps carries it. Move the app up (account.apps.order), delete an app ahead of it, or move to a plan that allows more
INVALID_ARGUMENT 400 account.apps.order with apps missing an app of the account, naming one twice, or naming an app that is not the account's (field: "apps"). Nothing changes

Regions

region.choices (GET /v1/_accounts/{id}/regions) lists the regions where an app can be created. Any account member can call it. Each region is returned as {"slug","name","country"}. A region appears only while it is offered and serving apps. The list is empty when no region is available. Pass a listed slug as region to account.app.create to place the app there:

{"name": "substrio_region_choices", "arguments": {"id": 1}}
# {"regions":[{"slug":"eu-central","name":"Frankfurt","country":"DE"}]}
{"name": "substrio_account_app_create", "arguments": {"id": 1, "slug": "shop", "region": "eu-central"}}
Code Status When
REGION_UNAVAILABLE 409 the region is not listed by region.choices: unknown, hidden, or not serving apps yet. Pick a listed one, or leave region out
REGION_FULL 409 every server in the region is at its app limit. Pick another region or try again later

Apps used inside other apps

An app is a database with an owner, and code is optional. Used inside another app, it is a piece. The piece brings the code, and the host brings the database.

Each collection has one home, and its author chooses it:

Home Where the data lives Who reaches it
private (the default) This app This app's code
shared This app This app's code, and the apps it offers the collection to (app calls)
hosted The app that uses this one This app's code, and the host's owner through an archive

The author answers one question: may the host's owner see and take this record? If the answer is yes, the collection can be hosted, and it travels with the host's app. If the answer is no, it stays private. A hosted collection is declared, so an owner can read what a piece keeps in their app (declaring what a jet keeps).

Keys

Kind Scope Who holds it
Account key One account: its members, apps, billing and the apps it owns a person or service acting for the account
App key One app: its data, functions, deploys, and config the app itself, embedded in its deploys and MCP clients
Agent token One signed-in user's access, through MCP or OAuth an AI agent or OAuth client acting as that user

Create an account key with account.key.create to administer the account, including its apps, members, and billing. account.app.create and tenant.key.rotate return an app key. It can read and write that app's data, deploy its functions, and administer that app. An agent token is created by exchanging an enrollment code (agent.enroll.create, then agent.enroll.exchange) or through MCP OAuth. It acts as the person it was created for wherever a session would, scoped to the person's role. See agents.

A key is shown in full exactly once, at creation or rotation. account.key.list shows a key's id, name, mode, creation time, request limits, and last use. tenant.key.list shows its id, creation time, expiry, and state (current, previous, or expired). Neither lists the key itself. account.key.revoke and tenant.key.revoke revoke a key immediately.

Creating an app and getting its key

curl -s -X POST https://api.example.com/v1/_accounts/1/apps \
  -H "Authorization: Bearer $SUBSTRIO_ACCOUNT_KEY" \
  -H 'content-type: application/json' \
  -d '{"slug":"shop"}'
# {"_id":2,"slug":"shop","subdomain":"shop","created":true,"key":"...","key_id":"..."}

The same call over MCP, sent with the account key:

{"name": "substrio_account_app_create", "arguments": {"id": 1, "slug": "shop"}}

Save the key field: it is the app's key, returned once, and every later call to the app (data, functions, deploys) uses it. See getting started for the first deploy, and MCP for connecting an agent to the new app.