Skip to Content
DocsAdmin GuideAdmin Console

Admin Console

The Admin console is where Quazzar staff run the platform: tenants, users, instances, billing, background jobs, support tickets, release notes and platform settings. It is a separate app (quazzar-cloud-admin) that serves the web UI and forwards /api/* calls to the Control Center, where every /api/admin/* endpoint lives.

Only staff accounts can sign in. A staff account is a Control Center user with a global role.


Finding your way around

  • Sidebar: Overview (Dashboard) · Tenants (All tenants, Organizations) · Users (All users, Instances) · Operations (Support tickets, Background jobs) · Billing (Analytics, Invoices) · System (Settings, Release notes, Audit log). You only see the pages your role allows. Support tickets shows the number of open tickets, Background jobs the number of failed jobs.
  • Account menu (your email and role at the bottom of the sidebar): Security keys & sessions and Sign out.
  • Top bar: breadcrumbs, the Filter this table… box (press / to jump to it, Esc to clear) that searches the table on the current page, and the environment badge, e.g. prod · eu-west.
  • Tables are paged on the server. The page, the sort order and the filters are part of the URL, so you can bookmark or share a filtered view. The Customers view of the old console is a tab on All tenants.
  • The dashboard’s KPI cells and Needs attention items are links to the matching filtered page.

Roles

Each staff member has one global role. The role decides which parts of the console they can open and which actions they can take.

CapabilitySuper adminAdminManagerSupport leaderSupport T2Support T1
Own session info✓✓✓✓✓✓
Support tickets: read, reply, internal notes, assign, escalate✓✓✓✓✓✓
Look up tenants, organizations, users, instances (read-only)✓✓✓✓✓✓
Read release notes✓✓✓✓✓✓
Dashboard (stats, needs attention, health)✓✓✓✓✓
Billing and invoices (read)✓✓✓✓✓
Billing changes (packages, retry payments, issue credits)✓✓✓
Tenant actions (create, suspend, reactivate, change plan, extend trial, release channel, reset MFA)✓✓✓
Background jobs (view, retry, cancel)✓✓✓
Audit log✓✓✓
Write release notes✓✓✓
Marketplace moderation✓✓✓
Read platform settings✓✓✓
Change platform settings✓
Grant or remove staff roles✓
Impersonate a tenant✓

A request the role doesn’t allow gets 403 {"error":"forbidden","required":"<capability>"}. Changing someone’s role signs them out everywhere, so the new role applies straight away. Super admins cannot demote themselves.


Signing in

  1. Enter your email and password.
  2. If you have a second factor, the console asks for it. You can use either:
    • a 6-digit code from your authenticator app (TOTP), or
    • a security key (WebAuthn / FIDO2, e.g. a YubiKey or a platform authenticator).
  3. The sign-in starts a staff session. The footer of the sign-in screen shows its terms: All actions are audited · session 8 h · IP ….

The second-factor step only works together with the password step that came right before it. It expires after 5 minutes.

Behind the scenes the password step returns an mfa_token, and POST /api/auth/totp/verify-login needs it together with the code. A bare user_id + code is refused with 401 {"error":"mfa_token_required"}. For staff accounts this is always enforced. For regular Control Center users it follows CC_TOTP_REQUIRE_MFA_TOKEN on the Control Center:

VariablePurposeDefault
CC_TOTP_REQUIRE_MFA_TOKENRequire the mfa_token from the password step on /auth/totp/verify-login for every user. Set to false only as a temporary fallback for old clients that still send a bare user_id. Staff accounts always need the token.true

Security keys

Security keys are optional. They are an alternative to TOTP, not a replacement. Add keys under account menu → Security keys & sessions once you are signed in. You can register several keys, rename them and remove them. The same page turns the authenticator app (TOTP) on or off.

The relying party (the domain keys are bound to) comes from the Control Center configuration:

VariablePurposeDefault
CC_ADMIN_URLAdmin console URL. Its origin is allowed and its host becomes the relying-party ID.none
CC_WEBAUTHN_RPIDOverrides the relying-party ID, e.g. quazzar.cloud to share keys across subdomains.host of CC_ADMIN_URL, else localhost
CC_WEBAUTHN_ORIGINSExtra allowed origins, comma separated.none
CC_PUBLIC_BASE_URL / CC_BASE_URLThe Control Center’s own URL. Its origin is allowed so every workspace user can register and use security keys there too. When it sits under the relying-party ID above it shares it; otherwise the Control Center gets its own relying party.none
CC_WEBAUTHN_CC_RPIDRelying-party ID for the Control Center when it is not under CC_WEBAUTHN_RPID (must be its host or a parent domain).host of the Control Center URL

Without any of these the Control Center uses a development setup: relying-party ID localhost with origins http://localhost:8090, :5173 and :5174.

Keys are bound to the relying-party ID. If you change CC_WEBAUTHN_RPID or move the console to another domain, staff must register their keys again. When the admin console and the Control Center use different relying parties (for example admin.quazzar.io and app.quazzar.cloud), a key registered in one only works there; register it in both, or set CC_WEBAUTHN_RPID to a shared parent domain.

Sessions

  • The staff session length comes from Settings → Access → Staff session length (default 8h).
  • Security keys & sessions lists your active sessions (browser, IP, start and expiry). Revoke any of them; revoking the current one signs you out.
  • Sign out revokes the session on the server. Every token issued for that session stops working at once, including ones already refreshed.
  • If a policy blocks you (see below), the console replaces its pages with a screen that explains what to do: register a security key or set up an authenticator app right there, then sign in again. A blocked network shows the address that was refused.

Security policies

Set these under Settings → Access (super admin only). Settings are validated as you type, and secrets such as the Gmail client secret are never shown: use Replace to set a new value.

SettingEffect
Staff session lengthHow long a staff session lasts (15 minutes to 7 days).
Require MFA for staffStaff who signed in without a second factor can’t use the console until they enrol TOTP or a security key and sign in again.
Require hardware key for staffOnly sessions started with a security key can use the console. TOTP sign-ins are refused.
Admin IP allow-listComma-separated CIDRs allowed to call the admin API. Leave empty to allow any address.
Impersonation needs a ticketImpersonating a tenant requires a support ticket from that tenant.

The admin console server can also restrict access by network before anything reaches the Control Center:

VariablePurpose
ADMIN_ALLOWED_CIDRSComma-separated CIDRs or IPs. Every request (UI and API) from anywhere else gets 403.
ADMIN_TRUST_PROXYtrue when the console runs behind a trusted ingress: the first X-Forwarded-For address is then used as the client IP.
ADMIN_ENV, ADMIN_REGIONLabel for the environment badge in the top bar, e.g. prod and eu-west. Served at GET /admin-env.

Tenants

New tenant

All tenants → New tenant (admins and managers) creates a workspace with its owner:

  • Tenant name, Owner email, Plan (from the billing packages), Workspace mode (business, MSP, family or school), Trial in days (empty or 0 = no trial) and an optional Region such as eu-west. The region is shown until the tenant’s nodes report their own location.
  • If the owner email already has a Quazzar account, the new workspace is added to it. Their password and second factor stay as they are, and the workspace appears in their workspace switcher.
  • Otherwise a new account without a password is created. The owner gets an invitation email. Opening the link and choosing a password (at least 8 characters) activates the account. Until then nobody can sign in with it.
  • The invitation link is valid for 7 days. Without email configured, the tenant and owner are still created, but no email is sent: the dialog says so.
  • The console opens the new tenant. The creation is written to its audit trail (admin.tenant.create).

Issue credit

On a tenant, Issue credit (billing write) gives the tenant money back:

  • Amount in USD, Apply to Next invoice (deducted from the next invoice only) or Account balance (drawn down across future invoices), and a Reason for the audit log.
  • With Stripe configured and a Stripe customer on the tenant, the credit is created in Stripe right away: a negative invoice item for Next invoice, a customer balance transaction for Account balance. It shows as in stripe.
  • Without Stripe, or before the tenant has a Stripe customer, the credit is recorded as pending and not deducted anywhere yet. Invoices only come from Stripe, so a pending credit has to be applied by hand once billing is connected.
  • If Stripe refuses the credit, it is recorded as failed with Stripe’s message.
  • The Credits card on the tenant lists every credit with its amount, target, reason, who issued it and its status, plus the applied and pending totals. Each credit is audited (admin.tenant.credit).

Release channel

⋯ → Move to canary channel puts a tenant on the canary release channel: its users see canary release notes in What’s New before everyone else (see Release notes). Move to stable channel reverts it. The channel is shown in the tenant’s facts and every change is audited (admin.tenant.release_channel).


Release notes

A release note is draft, canary or published:

StatusWho sees it in What’s New
DraftNobody.
CanaryOnly the canary audience: users of tenants on the canary release channel, users of tenants running a staged OS rollout to that version, and nodes whose rollout stage has already been dispatched.
PublishedEveryone on the note’s surfaces (OS, CC, mobile).

The list shows a colored chip for each status and can be filtered by it. On a note, Start canary moves a draft to canary. Promote · publish to all publishes a canary note. Back to draft and Unpublish take it back. The editor has the same three states. Every status change is audited (admin.release_note.publish, with the status before and after).

Notes that ship with the Control Center

The official Cloud OS and Control Center notes are written in the repository (docs/user-release-notes.md and docs/cc-release-notes.md) and compiled into the Control Center binary. On every start the Control Center adds or updates them, matched by product and version: title, summary, items, published status and a publish date equal to the release date. A deploy is all it takes for a new release to appear in What’s New; there are no SQL seeds to run.

A shipped note is rewritten only when its text in the repository changes. Edits made here to a shipped note stay until then, after which the repository version wins. Notes created here for a version the repository has no entry for (for example a canary note for an upcoming version, or mobile notes) are never changed or removed by a deploy.

Shipped notes are managed from the repository:

  • To hide a shipped note, Unpublish it (or move it back to draft) instead of deleting it. A deleted shipped note comes back on the next deploy.
  • If you create a note here for a version that later gets an entry in the repository (same product and version), the repository entry takes it over on the next deploy: its text replaces yours and it is published.

Audit log

Every change made in the console is written to the hash-chained audit log, including sign-in and sign-out. Each entry records:

  • actor: who did it (user, email, staff role, session),
  • action: for example admin.tenant.suspend, admin.settings.update, admin.ticket.escalate,
  • target: what was changed (type and ID),
  • before / after: a summary of the change (secrets are redacted),
  • IP address and user agent.

Changes to a tenant are chained into that tenant’s audit trail, so the tenant detail page lists what staff did to it. Platform-wide changes, such as settings or jobs, go into the staff member’s own organization trail.

Open System → Audit log to browse it. Filter by tenant, by actor (type part of an email in the top-bar filter) and by action, and click a row to see the before and after values. The API is GET /api/admin/audit?tenant_id=&actor=&action=&page=. actor accepts a user ID or part of an email address. An action ending in . or * matches a prefix, e.g. admin.tenant.*. Verify re-computes a tenant’s hash chain and reports the first entry that was tampered with.


Impersonation

Super admins can open the Control Center as a tenant’s owner to reproduce a problem:

  1. Open the tenant and choose Impersonate. Give a reason, and pick one of the tenant’s open tickets if your policy requires one.
  2. The Control Center opens as the tenant owner in a new tab (through a one-time /impersonate#token=… link). The dialog shows when the session expires and can open it again. The Control Center shows an amber Viewing as (owner) · staff session bar for as long as you are impersonating. Click End in that bar to sign out of the staff session; the tab closes (or returns to the sign-in page).

What the policy guarantees:

  • The impersonation token lasts at most 30 minutes and has its own server-side session. Signing out ends it at once.
  • It never carries a staff role, so it can’t be used to reach the admin console.
  • The start of each impersonation, with its reason and ticket, is written to the tenant’s audit trail. So is every change made during it, because tenant actions are audited too.
  • Suspended tenants cannot be impersonated.

Impersonate only to help the tenant, preferably with a linked support ticket. Turn on Impersonation needs a ticket to make that mandatory.


Support workflow

  • Replies are visible to the customer. The first reply sets the ticket’s first-response time.
  • Internal notes are only visible to staff.
  • Assign a ticket to any staff member, or to yourself. The list can be filtered to assigned to me.
  • Escalate raises the priority one step, assigns the ticket to the least busy support leader, adds an internal note and emails the leader.
  • Tickets show the tenant’s plan, the intake channel and the first-response SLA: urgent 1 h, high 4 h, normal 24 h, low 72 h.

Background jobs

Jobs show their progress, the worker that runs them and how long they took. The list refreshes every 10 seconds; click a job to see its payload, result and error. Retry failed re-queues every failed job (of the filtered type, if one is selected) in one request, up to 200 per run, and reports how many went back to the queue. Cancel stops a pending job at once. A running job stops at its next checkpoint. Retry re-queues a failed or cancelled job. Set CC_WORKER_ID to name a worker. The default is <hostname>-<pid>.

Billing

Invoices show the tenant, amount, due date, paid date and status. Retry asks Stripe to charge a failed invoice again. Retry failed does it for every failed invoice, or only the filtered tenant’s, in one request (up to 200 per run, oldest first). It then lists the invoices that still failed and why. Each row links to the invoice PDF and the hosted invoice page. Without Stripe configured it answers 503 stripe_not_configured and charges nothing. Analytics shows MRR, ARR, average revenue per account (ARPA), churn, the plan mix and new vs. churned tenants per month, all calculated from paid invoices.


Admin API reference (additions)

All endpoints are under /api/admin/ and need a staff token with the listed capability. Every mutation is audited.

MethodPathCapabilityBody → response
POST/admin/tenantstenants.write{name, owner_email, plan, mode, trial_days?, region?} → 201 {tenant, owner{user_id, account_id, email, existing_account}, invitation{id, expires_at, email_sent}}. Invalid input → 400 {"error":"validation_failed", "fields":{…}}
PUT/admin/tenants/{id}/release-channeltenants.write{"channel": "stable"|"canary"} → {release_channel}
GET/admin/tenants/{id}/creditsbilling.read?page&per_page → {items, total, page, per_page, summary{pending_cents, applied_cents, count}}
POST/admin/tenants/{id}/creditsbilling.write{amount_cents, reason, apply_to: "next_invoice"|"balance", currency?} → 201 {credit, applied_via: "stripe"|"local", message}; a Stripe error → 502 {"error":"stripe_error", credit}
PATCH/admin/release-notes/{id}/publishrelease.write{"status": "draft"|"canary"|"published"} (the older {"published": bool} still works) → {id, status, published, previous_status}
GET/admin/release-notesrelease.readalso accepts ?status=draft|canary|published; every note carries status
POST/admin/invoices/retry-failedbilling.write{tenant_id?} → {attempted, succeeded, failed, errors[{id, ref, error}], more}. succeeded counts invoices that ended up paid. Without Stripe → 503 stripe_not_configured
POST/admin/jobs/retry-failedjobs.write{type?} → {attempted, succeeded, failed, errors[{id, error}], more}

more: true means more than 200 rows matched: run the call again.