$ aoox

DocsStart here

Users & roles

Instance roles decide who can touch the infrastructure; per-project membership decides who can see and change which project.

Two layers
Instance roles + per-project membership
Invitations
One-time link, 7 days, no email
Security
TOTP 2FA, API tokens, audit log

Instance roles

As in the role select in the UI: Member — projects & deploys; Admin — + registry, servers, notifications, terminal; Owner — everything.

Allowed toowneradminmember
Projects they created or were assigned to✓✓✓
View & manage all projects✓✓–
Deploy, rollback, backups, jobs, volume/file mounts✓✓✓
Data browser: read✓✓✓
Own account: password, 2FA, API tokens✓✓✓
Data browser: write, SQL import, create extra databases✓✓–
Invite members (admin/member)✓✓–
Registry, remote servers, Swarm, notifications, Git credentials, S3 destinations✓✓–
Web terminal & bind mounts✓✓–
Audit log✓✓–
Invite owners, change roles, delete users✓––
Reset password & disable 2FA for members✓––
Provision/remove registry & proxy, GC, disk cleanup✓––
Init/leave Swarm & manage nodes, instance backup✓––
  • Roles are read from the database on every request: a role change or a deleted user takes effect immediately, without waiting for a session (7 days) to expire.
  • An owner can't delete or demote themselves if they're the last owner, and a user who still owns projects can't be deleted.

Per-project membership

Besides the instance role, every project has its own member list (the Members tab on the project page). This is what decides who can see that project:

WhoWhich projects they see
Instance owner & adminAll projects, without needing to be added.
The project's creatorThe project they created — always as a project admin.
Other instance membersOnly projects they've been added to; others return 404, not 403.
Project roleAllowed to
adminEverything a developer can, plus manage project members and delete the project.
developerCreate & change applications, databases, stacks, domains, mounts, jobs, backups — day-to-day work.
viewerRead only: status, logs, metrics, backup list. Every state-changing request is rejected, including via API token. Database passwords, database rows (and Redis/Valkey keys), SQL dumps, CSV exports and backup downloads are refused too: a viewer can see that a database exists and its structure, but cannot read or take away its data. The same goes for the environment variables and build args of applications and stacks (they are left out for viewers) and for the project export, which contains them.
  • Adding a project member uses the email of an account that already exists on the instance — invite them to the instance first if they don't have an account.
  • The viewer restriction is enforced centrally in the API for all project endpoints, not just hidden in the UI. Log/terminal tickets can still be read.
  • When this feature was rolled out, every existing member was backfilled onto every project so the upgrade didn't change anyone's access — adjust it afterward if you want to restrict things.

The first owner

MethodWhenNotes
/setupThe users table is still emptyA form for name, email, password. A serializable transaction — two people can't both become the first owner.
ADMIN_EMAIL + ADMIN_PASSWORDAt boot, if there are no users yetFor automated installs. Idempotent: never overwrites an existing account; safe to leave in the env.

Inviting members

  1. 01Inviteemail + role → link shown once
  2. 02Sendcopy the link, send it via chat
  3. 03Joinrecipient fills in name + password
  4. 04Sign inlogged in right away, invitation closed
  1. Settings → Members → Invite member

    Fill in Email and Role. Admins can only invite admins/members; owners can invite owners.

  2. Copy the link — it's shown only once

    https://panel.example.com/invite/9f3a1c…e21d

    Starts with WEB_ORIGIN; if that's still an IP, the link contains that IP. aoox doesn't send email — send it yourself via chat.

  3. The recipient opens the link

    The invitation page shows the email and role that were already set; the recipient only fills in a name and password (min. 8), then is signed in right away. The link becomes invalid once used.

Invitation rules

  • Valid for 7 days; one link = one account.
  • Inviting an email that already has a pending invitation replaces the old one (the old link stops working).
  • The Pending invitations list on the Members card shows the ones not yet accepted; they can be revoked.
  • The invitation-accept page is rate-limited to 10 attempts/minute per IP.

Managing members (owner)

ActionWhereNotes
Change roleRole select on the member's rowTakes effect on the next request.
Edit name or emailThe Edit name and email (pencil) icon on the member's rowChanges how they appear and which email they sign in with; no password is needed. Not for owners, and not for your own row (use Settings → Account). Rejected if the email is already used by an account or reserved by a pending invitation.
Reset passwordThe Set new password (reset) icon on the member's rowFor a member who forgot their password; deliver the temporary password through a secure channel and ask them to change it.
Disable a member's 2FAMember's row (non-owner)For someone who lost their authenticator and backup codes. An owner must use a backup code themselves.
Delete a userMember's rowRejected if they're the last owner or still own a project (move/delete the project first). Their invitations and API tokens go with them.
Remove from a single projectMembers tab on the project pageCan be done by a project admin alone; their user account stays.

Account security

The Account card in Settings, for every role:

Change name or email

Edit profile opens a form with your Name and Email. A new name needs no password. Changing the email also asks for your Current password, and the email must not belong to another account or to a pending invitation. From then on you sign in with the new email; your current session keeps working, and API tokens are not affected. Signed in with an API token you cannot change these. An owner changes other members' name and email in Settings → Team.

Change password

Fill in Current password + New password (min. 8). Other sessions aren't forced to log out.

Two-factor verification (2FA)

  1. Enable 2FA → scan the QR code

    Any TOTP app (Google Authenticator, Aegis, 1Password, Bitwarden).

  2. Enter the 6-digit code to confirm

    Once it's correct, 10 backup codes are shown once — save them in a password manager. Each code is single-use.

  3. The next sign-in becomes two steps

    Password → a code page (TOTP or a backup code). The second step must be completed within 5 minutes.

  • Disabling 2FA from the Account card requires the current password and a valid code — it can't be done from a stolen session alone.
  • Lost your authenticator: use a backup code; once they're all used up, ask an owner to disable your 2FA. A last owner without a backup code can't be recovered from the UI — don't let that happen.
  • API tokens aren't affected by 2FA (used without a second step).

API tokens

  1. API token card → give it a Name and an Expiry

    E.g. name it GitLab CI; expiry of 30 days, 90 days, 1 year, or never.

  2. Set permissions (optional): Read-only, Restrict to projects, variables

    Read-only rejects every state-changing request — same as the viewer role, including websocket log tickets. Restrict to projects picks one or more projects; the token then gets 404 outside them and is rejected on every instance Settings route (server, registry, proxy). The variables limit narrows which variable set environments it may read or change, as set/env, set/* or */env (see Variables).

  3. Copy the aoox_… token — shown only once

    Using the token
    curl -H "Authorization: Bearer aoox_…" \
      https://api.panel.example.com/applications/<id>/deploy -X POST
  • A token acts as its owner with the same role — permissions can only narrow access, never extend it beyond the owner's, even for an owner.
  • Scope is checked in one place before any route runs, so it also applies to the aoox CLI, which uses the same token.
  • Tokens are stored hashed; lost means create a new one. Revoke anytime from the list.
  • Endpoint docs (OpenAPI/Swagger) live at <PUBLIC_API_URL>/docs.

Audit log

Settings → Audit log (owner/admin): who did what, when, and from which IP — including sign-ins, deploys, env changes, invitations. Filter by action, e.g. deploy, /databases/, sign-in. Request bodies are stored with secrets redacted; entries older than 90 days are deleted automatically. The list shows actions in readable words (e.g. “Deploy application”); the original route is still visible in the details, and an action without a label is shown as it is. The ten latest entries also appear on the Recent activity card on the Dashboard (owner/admin).

Recommended practices

  • At least two owners, both with 2FA and saved backup codes.
  • Default new members to member; only raise them to admin when they need to manage infrastructure.
  • One API token per integration, with an expiry — revoke it when the integration is retired.
  • Check the audit log after an incident or when an unfamiliar deploy shows up.

Common pitfalls

SymptomCause & fix
Invitation link contains an IP / httpWEB_ORIGIN is still an IP. It still works; set up a panel domain first for a cleaner link.
Invitation isn't validAlready used, expired (7 days), revoked, or replaced by a new invitation for the same email.
Can't delete a userThey're the last owner, or still own a project — move/delete the project first.
Sign-in asks for a code but there's no authenticatorUse a backup code; once they're used up, ask an owner to disable 2FA.
API token gets 401Expired/revoked, or the header isn't Bearer aoox_…. Create a new token.
Not available yet
SSO/OAuth; sending invitations by email; transferring project ownership.

Next steps

Found a mistake? Edit this page on GitHub ↗