Skip to content

Team and roles

Who can sign in, what viewers, approvers and operators may do, setting up the first operator, and API tokens.

Each person signs in with their own email and password, and has one role. API tokens carry a role too. The roles are ordered, so each one can do everything the roles before it can:

Role Can
viewer See orders, flows, approvals and the back office; use the ops chat (its proposals still need a person with the right role to confirm)
approver Also approve and reject approvals
operator Also start, cancel and restart orders, deliver events, create, edit and publish flows, run the stock review, manage webhook subscriptions, and manage the team and API tokens

The roles and the order are ROLES in packages/core/src/roles.ts. Every route checks the role on the server; the dashboard also hides controls a role can’t use.

A deployment with no APP_PASSWORD and no people is open: everyone acts as an operator, which is the local development default. To turn sign-in on, open Set up sign-in in the menu (the Team page) and create your operator account with a password you choose. You are signed in as it, and from then on everyone signs in.

With APP_PASSWORD set, sign-in is on from the start. Sign in with no email and the workspace password to reach the Team page as an operator. The password stays a break-glass operator sign-in and API credential; it has no identity, so its decisions are recorded as operator.

Operators manage people and tokens at /team:

  • Add person: email, optional name and role. The page shows a generated 20-character password once; hand it over and ask them to change it on their Account page.
  • Role: change and save. You can’t lower your own role.
  • Reset password: shows a new one-time password and signs the person out everywhere.
  • Disable / Enable: a disabled person can’t sign in, and their sessions end.
  • Without APP_PASSWORD, the last active operator can’t be demoted or disabled, so someone can always manage the team.

Passwords must be 10 to 256 characters, not a common password and not contain the email address. They are stored as PBKDF2-SHA256 (100,000 iterations, the Workers limit) as pbkdf2:<iterations>:<salt>:<hash>.

Sessions last 30 days in a __Host-ol_session cookie that holds a random token; D1 stores only its SHA-256. Signing out deletes the session.

Failed sign-ins are counted in 15-minute windows, and an attempt over a limit is refused with 429 and a wait before any password is checked, even when it is right:

Counted per Failures allowed Stops
Email and address together 5 Guessing one person’s password from one place
Address 30 One place trying many accounts
Email 50 Many places trying one account; high, so nobody can easily lock a person out

A successful sign-in clears its email-and-address count. The workspace password counts as one email. Addresses come from cf-connecting-ip, which Cloudflare sets. The limits are LIMITS in apps/web/app/lib/throttle.ts, and the counts are D1 auth_failures.

Systems that call the REST API use a token:

curl https://orderloop.io/api/v1/orders -H "Authorization: Bearer ol_live_…"

Create one on the Team page with a name (what will use it), a role and how long it lasts: 30 days, 90 days (the default), 1 year or never. The token is shown once and stored only as a SHA-256; the list shows its first 12 characters, who created it, when it was last used and when it expires, highlighting tokens that expire within 14 days. Revoke it there. An expired token gets 401 with This API token expired on <date>…; create a new one and switch the integration over before the old one runs out. Tokens made before expiry existed never expire. A token’s role limits it like a person’s: an approver token can decide approvals but not start orders. Approval decisions made with a token are recorded as token:<name>.

Operators see security events at Audit log (linked from the Team page, /audit), newest first, filtered by kind, and through GET /api/v1/audit-events. Each event records when, who acted, what they acted on, details and the address:

Kind Actions
Sign-in auth.sign_in, auth.sign_in_failed (with the email tried and why: unknown email, wrong password, disabled, no password set, wrong workspace password), auth.sign_in_throttled, auth.sign_out, auth.password_changed
Team team.first_operator, team.user_added, team.role_changed (from and to), team.password_reset, team.user_disabled, team.user_enabled
API tokens token.created (role, prefix and expiresAt), token.revoked
API access api.bearer_failed and api.bearer_throttled, for wrong guesses at the shared password; api.token_expired, at most hourly per token, when an expired token is used

A person signing in only hears that the email and password didn’t match; the reason is for operators. Passwords and tokens are never recorded. Events are kept 180 days. Writing an event never blocks the action it records. Approval decisions are recorded on the approval itself (below), not here.

An approval’s by is the person who decided: their email for a signed-in person, token:<name> for an API token. On the order page’s live connection, the agent takes the name from the connection, never from what the page sends; see OrderAgent.

Changes reach pages that are already open. Each action on the order page’s live connection checks the person’s session again, so a lower role applies to the next action, and signing out, a password reset or being disabled ends it.

Every *.orderloop.io host counts as the same site, so the SameSite=Lax cookie would still be sent with a form post from a sibling subdomain. The web Worker therefore refuses a browser request that changes something (any method but GET, HEAD and OPTIONS), or opens the order page’s WebSocket, unless its Origin is the app’s own; without Origin it reads Sec-Fetch-Site, and accepts only same-origin or none. The answer is 403 { "error": "Cross-site request refused" }. Requests with a bearer token are exempt, since they carry no cookie, and so are clients that send neither header. The check is crossOrigin in apps/web/app/lib/origin.ts.

users.external_sub is reserved for linking an outside identity. Signing in through an OpenID Connect provider would match a person by that subject and start the same kind of session, keeping roles and tokens in OrderLoop.