Teams & orgs
DB-native identity: log in with a session, self-serve members, and share nodes across orgsslt_… tokens on Admin). The DB-native identity backend (the users/orgs/members routes below) is wired only when the deploy runs with POLYGON_STORE=postgres; without it, those routes return 404 and nothing changes for a token-auth deployment. Cross-org node sharing (last section) is independent — it works under token auth too.The model: users, orgs, sessions
With the DB backend on, a user is a person, an org is a tenancy root (a personal org is auto-created per user; teams are created on demand), and a session is the credential the SPA and API calls carry.
Login is two-step and passwordless:
1 An admin or org manager issues a one-time login token (slt_…) to a person.
2 They exchange it at POST /v1/auth/token for a session token (slyt_ses_…) — expiring, revocable, and stored only as a SHA-256 hash at rest. That session token is the bearer for every subsequent call.
Roles are owner · admin · member · viewer; a session’s access is bounded by its active org and its role’s scopes.
Log in: token → session
Exchange the login token for a session. The session token is returned once. A bad / expired / revoked login token is a 401.
curl -X POST $BASE/v1/auth/token \
-H "content-type: application/json" \
-d '{ "token": "slt_…" }'
# → {
# "session_token": "slyt_ses_…", # ← the bearer from here on; not shown again
# "expires_at": "2026-08-01T12:00:00Z",
# "user_id": "usr_…", "org_id": "org_…", "role": "member",
# "scopes": ["runs:*", …]
# }
# use it like any bearer:
curl $BASE/v1/auth/whoami -H "Authorization: Bearer slyt_ses_…"
# whoami is enriched with your role in the active org when the DB backend is on.
# revoke the current session:
curl -X POST $BASE/v1/auth/logout -H "Authorization: Bearer slyt_ses_…"logout is idempotent — an absent or unknown token is a silent success, so it never leaks token validity.
Seat the first human (operator)
Before any org session exists, the operator admin token bootstraps the first user with POST /v1/auth/seat — it creates the user (with an auto-provisioned personal org) and issues their first login token. Thereafter, owners/admins seat people through their org (below).
curl -X POST $BASE/v1/auth/seat \
-H "Authorization: Bearer $POLYGON_ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{ "name": "Ada Lovelace", "email": "ada@example.com" }'
# → { "user": {…}, "org_id": "org_…", "login_token": "slt_…" }Create a team & manage members
Any user session can create a team org (the caller becomes its owner). Owners and admins then seat members.
# create a team (caller = owner)
curl -X POST $BASE/v1/orgs \
-H "Authorization: Bearer slyt_ses_…" -H "content-type: application/json" \
-d '{ "name": "Acme Robotics", "slug": "acme" }'
# your org seats
curl $BASE/v1/orgs -H "Authorization: Bearer slyt_ses_…"
# invite/seat a member (owner/admin only) — a NEW user gets a one-time login_token back
curl -X POST $BASE/v1/orgs/<org_id>/members \
-H "Authorization: Bearer slyt_ses_…" -H "content-type: application/json" \
-d '{ "name": "Grace", "email": "grace@example.com", "role": "member" }'
# → { "user": {…}, "role": "member", "login_token": "slt_…" }
# list / remove members (owner/admin only)
curl $BASE/v1/orgs/<org_id>/members -H "Authorization: Bearer slyt_ses_…"
curl -X DELETE $BASE/v1/orgs/<org_id>/members/<user_id> -H "Authorization: Bearer slyt_ses_…"Guardrails are enforced fail-closed: an org you aren’t a member of returns 404 (never leaking existence); a member lacking owner/admin gets 403; an admin can’t grant owner/admin (only an owner escalates); and you can’t remove the last owner (409). Seating an existing account issues no credential — no impersonation.
POLYGON_ADMIN_TOKEN) is a cross-org superuser for these routes; a regular session is always confined to the orgs it’s a member of.Share your nodes with another org
By default a node belongs to the org that enrolled it, and only that org can place runs on it. An unowned (legacy) node is universal — any org may use it. To let another org run on your org’s nodes, issue a node-access grant: the scheduler then treats the grantee like an owner for the matched nodes. With no grants, scheduling is byte-for-byte unchanged.
A grant names a grantee_org and a selector for which of your nodes it covers:
allnodetiercommunity · partner · trusted)# grant org_beta access to your trusted-tier nodes until a date (needs nodes:grant;
# owner/admin hold it via "*"). owner_org defaults to your org.
curl -X POST $BASE/v1/nodes/grants \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{
"grantee_org": "org_beta",
"selector": { "kind": "tier", "tier": "trusted" },
"expires_at": "2026-12-31T00:00:00Z"
}'
# → 201 { "id": "grant_…", "owner_org": "org_acme", "grantee_org": "org_beta",
# "selector": { "kind": "tier", "tier": "trusted" }, "created_at": "…" }
# see grants your org issued + received
curl $BASE/v1/nodes/grants -H "Authorization: Bearer slt_…"
# → { "issued": [ … ], "received": [ … ] }
# revoke (owner org only) — effective on the next scheduler tick
curl -X DELETE $BASE/v1/nodes/grants/grant_… -H "Authorization: Bearer slt_…"You can only share nodes your org owns — owner_org is forced to your org (a superuser may name any), and a node selector for a node you don’t own is rejected. A caller that doesn’t own a grant gets 404 on read/revoke (no existence oracle). Node sharing is enabled per deployment — otherwise these routes return 503.