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 to | owner | admin | member |
|---|---|---|---|
| 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:
| Who | Which projects they see |
|---|---|
| Instance owner & admin | All projects, without needing to be added. |
| The project's creator | The project they created — always as a project admin. |
| Other instance members | Only projects they've been added to; others return 404, not 403. |
| Project role | Allowed to |
|---|---|
| admin | Everything a developer can, plus manage project members and delete the project. |
| developer | Create & change applications, databases, stacks, domains, mounts, jobs, backups — day-to-day work. |
| viewer | Read 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
| Method | When | Notes |
|---|---|---|
/setup | The users table is still empty | A form for name, email, password. A serializable transaction — two people can't both become the first owner. |
ADMIN_EMAIL + ADMIN_PASSWORD | At boot, if there are no users yet | For automated installs. Idempotent: never overwrites an existing account; safe to leave in the env. |
Inviting members
- 01Inviteemail + role → link shown once
- 02Sendcopy the link, send it via chat
- 03Joinrecipient fills in name + password
- 04Sign inlogged in right away, invitation closed
Settings → Members → Invite member
Fill in Email and Role. Admins can only invite admins/members; owners can invite owners.
Copy the link — it's shown only once
https://panel.example.com/invite/9f3a1c…e21dStarts with
WEB_ORIGIN; if that's still an IP, the link contains that IP. aoox doesn't send email — send it yourself via chat.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)
| Action | Where | Notes |
|---|---|---|
| Change role | Role select on the member's row | Takes effect on the next request. |
| Edit name or email | The Edit name and email (pencil) icon on the member's row | Changes 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 password | The Set new password (reset) icon on the member's row | For a member who forgot their password; deliver the temporary password through a secure channel and ask them to change it. |
| Disable a member's 2FA | Member's row (non-owner) | For someone who lost their authenticator and backup codes. An owner must use a backup code themselves. |
| Delete a user | Member's row | Rejected 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 project | Members tab on the project page | Can 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)
Enable 2FA → scan the QR code
Any TOTP app (Google Authenticator, Aegis, 1Password, Bitwarden).
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.
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
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.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).Copy the aoox_… token — shown only once
Using the tokencurl -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
| Symptom | Cause & fix |
|---|---|
| Invitation link contains an IP / http | WEB_ORIGIN is still an IP. It still works; set up a panel domain first for a cleaner link. |
| Invitation isn't valid | Already used, expired (7 days), revoked, or replaced by a new invitation for the same email. |
| Can't delete a user | They're the last owner, or still own a project — move/delete the project first. |
| Sign-in asks for a code but there's no authenticator | Use a backup code; once they're used up, ask an owner to disable 2FA. |
| API token gets 401 | Expired/revoked, or the header isn't Bearer aoox_…. Create a new token. |
Next steps
Found a mistake? Edit this page on GitHub ↗