Console
Docs for Database administrator
On this page
  1. Before you start
  2. Steps
  3. 1. Choose the tenant's identifiers
  4. 2. Register the tenant and its identity provider
  5. 3. Configure the tenant's gateway
  6. 4. Connect Data, if the tenant uses it
  7. 5. Register the first principal
  8. 6. Give the first grant
  9. 7. Sign in for the first time
  10. 8. Give the tenant's operator a credential
  11. Check that it worked
  12. If something goes wrong
  13. Related

Onboard a tenant

Bring a new customer onto the platform: register it and its identity provider in Access, connect its gateway, register its first principal and grant, hand its operator a credential of their own, and prove that a request through the gateway is allowed. A platform operator does this (see sysops), with a security engineer issuing the secrets.

Read byPlatform operatorApplication developerConsole and CLI user

Before you start#

You needWhy
An Access deployment in production, with ACCESS_SECRET_ENCRYPTION_KEY setWithout that key Access refuses to write any tenant (Access spec §4.3).
A platform administration credential (ACCESS_ADMIN_TOKEN, or a null-scoped entry of ACCESS_ADMIN_TOKENS)Only a platform credential may create a tenant (Access spec §10.1).
The tenant's OpenID Connect provider: its https issuer, and a client registered there for Access with its id and secretAccess logs callers in at that provider. It must support PKCE, and in production its issuer must resolve to public addresses (Access spec §6.1).
Access's callback, {ACCESS_PUBLIC_URL}/oauth/tenant/callback, registered at that providerThe provider sends the browser back there.
A gateway deployment for this tenant, and the public URL it is reached underOne gateway deployment serves one tenant (Access spec §1.4); see Deploy the platform.
The workspace UUID the tenant will use, and the gateway route id that serves itGrants name both.
The provider's sub for the first person to sign inPrincipals are keyed by it, never by e-mail (Access spec §6.2).

On Azure the provider of the tenant the deployment creates is Entra ID, and the deployment holds the client of Access at it: the application of Access, with a secret of its own and with the callback of Access as its only redirect URI. The outputs of the deployment carry the issuer of the directory, the identifier of the application, its secret and the callback of Access, and the registration of the tenant carries the four of them (The deployment of the platform on Azure).

The provider of Entra ID belongs to that tenant and to no other one. The provider of a tenant is the tenant's own: the tenant below is registered with the provider it names, and a tenant of the platform that signs in at another provider — a broker of another vendor, a directory of another cloud — is registered with that provider. The deployment takes no provider back: a tenant of the deployment that has a provider of its own keeps it, and the wizard registers the tenant without naming a provider. | curl and jq, or the CLI (platformctl) with PLATFORMCTL_ADMIN_TOKEN and an Access address in its profile | Every step below is an administration call (CLI usage). |

The examples use the tenant acme, Access at https://access.example.com and the tenant's gateway at https://gateway-acme.example.com:

bash
export ACCESS=https://access.example.com
export ACCESS_ADMIN_TOKEN=…      # a platform credential
export WORKSPACE=11111111-1111-4111-8111-111111111111

Steps#

1. Choose the tenant's identifiers#

Write these down before the first call; several services must agree on them later.

ValueExampleRule
Slugacme[a-z0-9-]{2,63}, the row's key in Access.
Gateway client idgateway-acmeUnique across all tenants; a second tenant claiming it gets 409 conflict.
Gateway client secretfrom your secret managerStored encrypted by Access and never returned.
Gateway callbackhttps://gateway-acme.example.com/auth/callbackMust equal the gateway's GATEWAY_OIDC_REDIRECT_URI exactly.
E-mail domainacme.exampleOptional and unique; only a hint for shared clients (Access spec §5.2).

2. Register the tenant and its identity provider#

One upsert, keyed by slug, records both the tenant's provider and its gateway client (API: register a tenant):

bash
curl -sS -X POST $ACCESS/v1/admin/tenants \
  -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"slug":"acme","displayName":"Acme Inc",
       "issuer":"https://login.acme.example",
       "idpClientId":"access-acme","idpClientSecret":"…",
       "idpRedirectUri":"https://access.example.com/oauth/tenant/callback",
       "emailDomain":"acme.example",
       "clientId":"gateway-acme","clientSecret":"…",
       "clientRedirectUris":["https://gateway-acme.example.com/auth/callback"]}' | jq .

The same call with the CLI:

bash
platformctl access tenants create --slug acme --display-name "Acme Inc" \
  --issuer https://login.acme.example \
  --idp-client-id access-acme --idp-client-secret "$IDP_CLIENT_SECRET" \
  --idp-redirect-uri https://access.example.com/oauth/tenant/callback \
  --email-domain acme.example \
  --client-id gateway-acme --client-secret "$GATEWAY_CLIENT_SECRET" \
  --client-redirect-uri https://gateway-acme.example.com/auth/callback

In the console, a platform operator creates the tenant from Operators (console user guide).

Keep the defaults unless you have a reason: autoProvision stays false, so nobody gets a principal just by signing in, and sharedClient stays false, so the tenant's gateway logs in only this tenant's people. Note the id in the answer: it is the tenant UUID that Data and the access context credentials use. A 201 means created, a 200 means an existing slug was updated.

A tenant that provisions its identities is a tenant whose first account needs no registration by hand: --auto-provision creates the principal at the first login, and every identity of the tenant that signs in afterwards is provisioned as well. The principal that is created has no grant at all: the login succeeds and every request is refused until the operator gives that principal a right (the first grant).

bash
platformctl access tenants create --slug acme --display-name "Acme Inc" \
  --issuer https://login.acme.example \
  --idp-client-id access-acme --idp-client-secret "$IDP_CLIENT_SECRET" \
  --idp-redirect-uri https://access.example.com/oauth/tenant/callback \
  --email-domain acme.example \
  --client-id gateway-acme --client-secret "$GATEWAY_CLIENT_SECRET" \
  --client-redirect-uri https://gateway-acme.example.com/auth/callback \
  --auto-provision

The upsert changes the flags the body names and resets the ones it does not: repeating the call without --auto-provision turns the provisioning of the tenant off, and --no-auto-provision says it in the words of the operator.

A tenant that hands its administration to its first identity is a tenant whose first account needs no operator at all: --auto-provision-admin gives the administration of the tenant to the first identity that signs in, and that identity may then add the users and the rights of the tenant (Access spec §6.4). The flag is off by default, and the identities that come after the first one are given nothing.

A tenant that shares its provider with another tenant is given nothing: the provider of a shared directory is the provider of several tenants, and the first identity that signs in at it is the first identity of the directory and not of the tenant. Give such a tenant its own provider, or grant the administration of the tenant by hand.

bash
platformctl access tenants create --slug acme --display-name "Acme Inc" \
  --issuer https://login.acme.example \
  --idp-client-id access-acme --idp-client-secret "$IDP_CLIENT_SECRET" \
  --idp-redirect-uri https://access.example.com/oauth/tenant/callback \
  --email-domain acme.example \
  --client-id gateway-acme --client-secret "$GATEWAY_CLIENT_SECRET" \
  --client-redirect-uri https://gateway-acme.example.com/auth/callback \
  --auto-provision --auto-provision-admin

Two things are needed for that identity to administer the tenant in the console: the console must hold a credential of the tenant (or of the platform), and the credential of the access context that the console reads must be allowed to ask about the tenant. The first identity of a tenant is the administrator of its tenant, and not of the platform: the console opens the pages of the tenant of the caller and no other.

A wrong issuer, client id or callback can be fixed by repeating the call, but Access keeps the provider client for ten minutes (Access spec §6.6), so wait that long or roll the replicas.

The secret of the provider is the one field a change may leave out: a call that names no idpClientSecret keeps the secret the tenant has, and the caller that changes the name of a tenant does not have to know it. A tenant that is created is created with a provider — a call that names no issuer, no client and no secret of the provider is refused with 400 invalid_request.

3. Configure the tenant's gateway#

The gateway treats Access as its only identity provider and its only decision service. Four values must match the registration character for character (Access spec §13.2):

bash
GATEWAY_OIDC_ISSUER=https://access.example.com          # = ACCESS_ISSUER
GATEWAY_OIDC_CLIENT_ID=gateway-acme                     # = clientId
GATEWAY_OIDC_CLIENT_SECRET=…                            # = clientSecret
GATEWAY_OIDC_REDIRECT_URI=https://gateway-acme.example.com/auth/callback
GATEWAY_OIDC_SCOPES=openid,profile,email,groups
GATEWAY_OIDC_ROLES_CLAIM=roles
GATEWAY_ACCESS_URL=https://access.example.com
GATEWAY_ACCESS_TOKEN=…                                  # = ACCESS_DECISION_TOKEN

The reference gateway authenticates its decision calls with a bearer token only, so GATEWAY_ACCESS_TOKEN carries Access's shared, unscoped ACCESS_DECISION_TOKEN (Access spec §8.8). The docs do not describe how to make the reference gateway present the tenant's client id and secret on decisions instead.

A production gateway also needs its session secret, identity signing key, a rate limit and shared state; the full list is in gateway configuration: required in production.

The route that serves the tenant's workspace needs a workspace placeholder, and its id is the routeId your grants will name (gateway spec §3):

json
{
  "id": "workspace-api",
  "match": { "host": "gateway-acme.example.com", "path": "/ws/{workspaceId}/" },
  "upstream": "http://upstream-b:3000",
  "auth": { "mode": "any", "workspace": { "param": "workspaceId", "requireUuid": true } }
}

Publish the table with platformctl routes apply ./routes.json or the control plane (gateway operations: changing the route table). Connecting an upstream is covered in Put a service behind the gateway.

4. Connect Data, if the tenant uses it#

Data serves a workspace only after two things exist:

  1. The workspace is registered in Data under the tenant UUID from step 2 (Data operations: workspaces):
bash
   DATA_OWNER_DATABASE_URL=… bun run workspace -- create \
     --id $WORKSPACE --tenant <access tenant uuid> --slug acme --name "Acme"
  1. Data's access context credential may ask about this tenant. Unless that token has the platform-wide null scope, add the tenant UUID (not the slug) to its list in ACCESS_CONTEXT_TOKENS and roll Access (Access configuration: access context credentials):
json
   {"the-token-of-the-backend": ["22222222-2222-4222-8222-222222222222", "<access tenant uuid>"]}

Data's binding for the route must carry the same routeId as the gateway route (Data configuration: bindings file). Building on Data is covered in Build an app on Data.

5. Register the first principal#

A principal is the platform identity of one external account, keyed by the tenant and the provider's sub (API: register a principal):

bash
USER_ID=$(curl -sS -X POST $ACCESS/v1/admin/users \
  -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"tenant":"acme","externalSubject":"00u1a2b3c4d5","email":"alice@acme.example","displayName":"Alice"}' \
  | jq -r .id)
bash
platformctl access principals create --tenant acme --subject 00u1a2b3c4d5 \
  --email alice@acme.example --display-name Alice

For a whole team, send up to 1 000 principals in one batch: platformctl access principals import --file ./principals.json, or the console's Users page with one identity, e-mail, name line per person. Each item succeeds or fails on its own (API: batch of principals).

6. Give the first grant#

A principal with no grant is refused everywhere. Grant a permission on the workspace and the gateway route from step 3 (API: grants):

bash
curl -sS -X PUT $ACCESS/v1/admin/grants \
  -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"tenant":"acme","userId":"'"$USER_ID"'","permission":"workspace.read",
       "workspaceId":"'"$WORKSPACE"'","routeId":"workspace-api","grantedBy":"ops@example.com"}'
bash
platformctl access grants put --tenant acme --user-id "$USER_ID" --permission workspace.read \
  --workspace-id "$WORKSPACE" --route-id workspace-api --granted-by ops@example.com

In the console this is the Rights page. Groups, roles, expiry and revocation are in Manage access.

7. Sign in for the first time#

Open https://gateway-acme.example.com/auth/login in a browser. The gateway sends the browser to Access, Access resolves the tenant from the client id and forwards it to the tenant's provider, and the browser comes back to the gateway with a session (Access spec §5.4). The token the gateway receives names the principal's UUID as its subject and lists the live grants' permissions in roles.

To let the tenant's people use the console as well, add {CONSOLE_PUBLIC_URL}/auth/callback to the tenant's clientRedirectUris (console operations: set up Access). The CLI's sign-in likewise needs its local callback allowed by the tenant (CLI usage: sign in).

8. Give the tenant's operator a credential#

Instead of sharing the platform token, add a tenant credential to ACCESS_ADMIN_TOKENS and roll Access. Its scope names the slug and the operator, so the audit records a person rather than a fingerprint (Access configuration: administration credentials):

json
{
  "the-token-of-the-operator-of-the-platform": null,
  "the-token-of-the-operator-of-acme": {"tenant": "acme", "operator": "alice"}
}

Each token is at least 16 characters. ACCESS_ADMIN_TOKENS has no _FILE form in this release, so set it in the environment. With this credential the operator can manage acme's principals, groups, roles and grants, but cannot create tenants, touch another tenant, or change sharedClient or default.

For the console, also put the token in CONSOLE_TENANT_ADMIN_TOKENS ({"acme":"…"}) and grant the operator console.admin on the console route with the slug as workspace (console configuration: what it needs from Access):

bash
curl -sS -X PUT $ACCESS/v1/admin/grants \
  -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" -H "content-type: application/json" \
  -d '{"tenant":"acme","userId":"'"$USER_ID"'","permission":"console.admin","workspaceId":"acme","routeId":"console"}'

Check that it worked#

  1. Ask Access the question the gateway will ask. The subject is the principal UUID, not the provider's sub; the CLI exits 0 on an allowance and 6 on a denial (CLI spec §6.2):
bash
   platformctl access decide --subject "$USER_ID" --workspace "$WORKSPACE" --route workspace-api

   curl -sS -X POST $ACCESS/v1/decisions \
     -H "authorization: Bearer $ACCESS_DECISION_TOKEN" -H "content-type: application/json" \
     -d '{"subject":"'"$USER_ID"'","workspaceId":"'"$WORKSPACE"'","routeId":"workspace-api"}'
   # {"allow":true,"reason":"allowed"}
  1. After the first sign-in, the principal's lastLoginAt is set and the tenant's counts have grown:
bash
   curl -sS "$ACCESS/v1/admin/users?tenant=acme&email=alice@acme.example" \
     -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" | jq '.principals[0].lastLoginAt'
   curl -sS $ACCESS/v1/admin/tenants -H "authorization: Bearer $ACCESS_ADMIN_TOKEN" \
     | jq '.tenants[] | select(.slug=="acme") | {userCount, grantCount}'
  1. In the signed-in browser, open a path under https://gateway-acme.example.com/ws/<workspace>/. A response from the upstream, not a 403 workspace_forbidden, proves login, decision and forwarding together. With Data behind the route, platformctl data me --workspace "$WORKSPACE" shows the principal, tenant and route Data sees.
  2. On the metrics, access_logins_total{tenant="acme",result="success"} and access_decisions_total{tenant="acme",result="allow"} rise while gateway_auth_failures_total stays flat.
  3. The audit holds tenant.created, principal.created and grant.created for the tenant: platformctl access audit list --tenant acme.

scripts/cli/admin-smoke.ts exercises all eighteen administration calls against a deployment. It works on its own tenant (ACCESS_SMOKE_SLUG, default smoke), not on acme (Access configuration: variables of the scripts and tests).

If something goes wrong#

SymptomWhere to look
The tenant call answers 409 conflict, 403 forbidden or 400 invalid_requestAccess troubleshooting: administration refusals
A tenant cannot be written at allAccess troubleshooting: administration refusals (encryption key missing)
The browser stays on an Access page with 400 invalid_request, 403 tenant_disabled, 502 upstream_unavailable or 403 identity_rejectedAccess troubleshooting: failed logins
403 identity_unknown at first sign-inAccess troubleshooting: unknown identity
The gateway refuses Access's discovery document, or tokens stop verifyingAccess troubleshooting: token and JWKS problems
The gateway callback answers 400 login_failedGateway troubleshooting: login callback
Signed in, but requests get 403 workspace_forbiddenGateway troubleshooting, then Access troubleshooting: decisions denied
Requests get 503 access_unavailableGateway troubleshooting
Data answers 401 UNAUTHENTICATED, 403 FORBIDDEN or 503 AUTHORIZATION_UNAVAILABLEData troubleshooting, and Access troubleshooting: access context refusals
A platformctl command refusesCLI usage: when something refuses