---
title: User Management
---

# User Management

## What it is

User Management is an **owner/admin-only admin console** for running an Omniscio _organization_ — inviting people, reviewing who's asking to join, setting each person's role and subscription tier, disabling or deleting accounts, and managing workspace membership for Team Chat.

**Read this caveat first, because it changes who this page is even for:** User Management is part of Omniscio's optional cloud identity layer ("Global Auth"), which is **off on the vast majority of installs**. On a normal single-user install there's no organization, no other users, and no reason for this panel — so it doesn't appear at all. The Settings row itself shows for **any** signed-in user once the Global Auth sign-in gate is turned on, but the full admin **console described on this page** — the Users / Access Requests roster, invites, role and tier changes — only renders for a global `owner` or `admin`. A plain member sees the same row labeled **Workspaces** and opening only the self-serve workspace surface (list + switcher + Create), not this console. If you're an individual user wondering where the admin console is, the honest answer is: it's an administrator feature for multi-person orgs, and it's almost certainly not relevant to you.

For the admins it _is_ for, it's the one place to manage the whole membership of your org from inside Omniscio.

## Where to find it

**Where it lives:** **Settings → User Management** (grouped under "Accounts & AI", or under a "Team & Admin" group in the newer settings layout). The nav row renders for **every signed-in user**, but its label and contents are role-shaped: global `owner`/`admin` staff see the full **User Management** console described here (three tabs — **Users**, **Access Requests**, and **Workspace** — with **Invite User** and **Bulk Invite** buttons in the header); a plain member sees the same row labeled **Workspaces**, opening the workspace surface only (their workspace list + switcher + **Create**, no tabs and no invite buttons — see [Self-serve workspaces](#self-serve-workspaces) below). Signed-out, the row is hidden.

## How it behaves

### How to use it

- **Invite one person.** Click **Invite User** → enter their **Email** (required) and optional **Display Name**, pick a **Role** (Owner / Admin / Member / Trial) and a **Tier** (Enterprise / Pro / Free) → **Invite**. This pre-creates their account so they skip the request-to-join step when they first sign in, and sends them an invite email (from `invites@amcmailbox.com`, linking the download page). The invitee then appears in the Users tab as a labeled **Invited** row until they first sign in.
- **Invite many at once.** Click **Bulk Invite** → paste a list of emails separated by commas, spaces, or new lines → **Invite (N)**. Re-running is safe: already-invited addresses are skipped, and you get a per-email summary of _invited / skipped / errored_. Each successfully-invited address also gets the invite email.
- **See and revoke pending invites.** An invited-but-not-yet-signed-in person shows as an **Invited** row (amber pill, em-dash last-login, amber status dot; there's also an "Invited" option in the Status filter). Its **⋮** menu has exactly one action — **Revoke Invite** — which deletes the invite _and_ any pending access request from the same email. Role/tier/disable/delete are deliberately unavailable on a pending row.
- **Approve or deny a request to join.** The **Access Requests** tab (with an amber count badge when any are pending) lists each request as a card — name, email, "Requested X ago" — with **Approve** and **Deny**. Approve pre-creates the user (member/free), clears the request, and emails them that their access was approved; Deny just clears it.
- **Change a role or tier, or disable/remove someone.** On the **Users** tab, filter by search / role / tier / status, then use a row's **⋮** menu: **Change Role ▸**, **Change Tier ▸**, **Enable / Disable Account**, or **Remove User** (confirm-gated). Every one of these is disabled on **your own row** — you can't change, disable, or remove yourself (last-owner and self-lockout protection). The roster is **paginated** — 50 users per page with **Prev / Next** controls below the table — and the search box, the Role / Tier / Status filters, and column sorting are all applied **by the server** (search still matches anywhere in the name or email), so you page through matches rather than filtering a single fully-loaded list.
- **Manage workspace membership.** The **Workspace** tab lists your org's Team Chat members and lets you assign or unassign users to the org, with an org role of `org_admin` or `member`. A member still pending an invite shows an **Invited** pill (instead of a role) and its per-row action reads **Revoke invite** rather than Remove; every row action (Make admin / Make member / Remove / Revoke invite) shows a busy state while the change applies so it can't be double-fired.
- **Role and org changes reach the affected person automatically — no re-login.** When you change someone's role/tier or their workspace assignment, their running app picks the change up on its own: the server re-mints their identity claims, and their app broadcasts the new identity the next time it silently refreshes its session token (a window reload triggers an immediate re-check; a long-idle app catches up on its periodic re-verification, at most a few hours). Only **Disable Account** forcibly ends their session. Changing a user's global role also never demotes a workspace they **own** — a self-serve workspace creator stays its owner.

## For agents

### How it works

**The visibility rule you see is only cosmetic.** The nav row is drawn for every signed-in user in [Settings-section-groups.tsx](../../src/renderer/src/features/settings/Settings-section-groups.tsx), with `globalAuthRole === 'owner' || globalAuthRole === 'admin'` choosing the **User Management** label and the full console versus the member-facing **Workspaces** label and workspace-only surface. That role check controls what the _menu entry says and shows_ — it is **not** the security boundary.

**The real boundary is server-side and cryptographic.** Every operation in the panel runs a fast local `assertCaller*` pre-check ([profile-authz.ts](../../src/main/services/global-auth/profile-authz.ts)) and then routes the read/write through a token-verifying Cloud Function (`globalAuthAdmin` / `globalAuthProfile`, project the shares backend runs on) that **re-verifies authorization on the server** using the caller's Firebase ID-token claims. `assertCaller` reads role/tier from the _cryptographically-verified_ token claims — not the machine-editable local cache — so a tampered client can't forge its way past it. **Which pre-check runs depends on the tab**, and the two are deliberately different:

- **Global-admin ops** (the Users and Access Requests tabs — invite, bulk-invite, change role/tier, disable, remove) call `assertCallerIsAdmin()`, asserting the **`manage_users`** capability: minimum role `admin`, full stop.
- **Workspace-tab org ops** (`listOrgMembers`, `assignUserOrg`, `bulkInviteToOrg` in [admin-operations.ts](../../src/main/services/global-auth/admin-operations.ts)) call `assertCallerCanManageOrgMembers()`, asserting **`manage_org_members`**: minimum role `admin` **OR** `orgAdminGrants` — a caller whose verified claim carries `orgRole === 'org_admin'` passes regardless of their global role. That is the point: it is how a workspace owner/admin manages their **own** org's roster without being global staff (see the org-admin scoped panel below). So a plain global `member` who is an org-admin **can** use the Workspace tab, and **cannot** touch anything on the Users tab.

Both capabilities are defined in the entitlements policy ([entitlements.ts](../../src/shared/entitlements.ts)). The admin operations live in [admin-user-operations.ts](../../src/main/services/global-auth/admin-user-operations.ts) (the global-admin ops — off-shipped, see the next paragraph) alongside the org-scoped roster ops in [admin-operations.ts](../../src/main/services/global-auth/admin-operations.ts); the IPC surface is [global-auth-handlers.ts](../../src/main/ipc/global-auth-handlers.ts) (the global-admin handlers are compile-time-gated); the renderer state is [user-admin-store.ts](../../src/renderer/src/stores/user-admin-store.ts) for the admin console and [user-management-store.ts](../../src/renderer/src/stores/user-management-store.ts) for the shipping workspace/org surface.

**The desktop admin console is compile-time OFF-SHIPPED from the packaged build (2026-07-21).** Omniscio is an Electron app, and a shipped build is trivially `asar extract`-able — so a runtime role gate hides the console from a casual user but is **not** confidentiality. The whole owner/admin console (the Users / Access Requests roster, the global **Invite User** / **Bulk Invite** tools, role/tier changes, the all-orgs list, the Team-Chat add-members picker, and the **invite-only mode** toggle) is now built behind an `INTERNAL_ONLY` guard and tree-shaken OUT of the production bundle — it renders only in internal/dev builds. Staff manage users from the separate **admin webapp** (`firebase/admin-dashboard`) instead. What still ships to every customer is the org-scoped workspace surface (a plain member's **Workspaces** list + switcher + Create; an org-admin's own-workspace roster). Server-side authz is unchanged — the strip removes the desktop tooling, not the security boundary. Full detail: [offship-admin-surfaces-contract.md](../../.claude/memory/contracts/offship-admin-surfaces-contract.md).

**The role/tier model** (source of truth: [paid-offering.ts](../../src/shared/paid-offering.ts) for the tier/limit values, re-exported by [entitlements.ts](../../src/shared/entitlements.ts), which holds the roles/policy):

- **Roles**, least → most privileged: `trial` < `member` < `admin` < `owner`.
- **Tiers**, low → high: `free` < `pro` < `team` < `enterprise`. (`team` is the per-seat collaboration rung, above `pro`. There is **no `starter` tier** — it was retired; if you see one in an older doc or a stale admin-granted value, it is not in `TIER_RANK`.)
- **Org role** (within a Team Chat workspace): `org_admin` or `member`.
- Unknown/corrupt values defensively coerce to the **least** privilege (`member` / `free`), so a bad stored value can never widen access.

**Removing a user is a "kick", by design (2026-07-17 decision).** It removes their profile document but **not** their underlying Firebase Auth login — the person can sign in again and start fresh (or re-request access when invite-only is on). The confirm dialog says exactly this and points to **Disable Account** as the tool that actually blocks access. Relatedly, the per-request disabled check **fails closed**: if the server can't verify whether an account is disabled (a database blip), the request is refused with a retry message rather than letting a possibly-disabled account act. And the whole console is meaningful **only when the Global Auth gate is active**: when it's off (the default), Omniscio treats the local user as the sole "operator" and there simply are no other users to manage.

**The Users list is capped server-side** (5,000 rows). Past the cap, the tab shows a warning banner ("Showing the first 5,000 users…") instead of silently truncating.

One clarification worth flagging: the **tier** here (`free`/`pro`/`team`/`enterprise`, your Omniscio subscription level) is a _different_ concept from the Claude **account plan tier** (Max/Pro/Free) used to pick a session's default model — see the note in [account-tier-model-defaults.md](account-tier-model-defaults.md). Same word, unrelated setting.

### Self-serve workspaces

A newer layer that lets a customer run their _own_ workspace instead of relying on staff to provision one. It is registered as the `self-serve-workspaces` feature (`settingKey: selfServeWorkspacesEnabled`) and was **shipped on 2026-07-19** — it shows for every eligible signed-in user, no Lab toggle needed. The whole thing is **all-Firebase** by an explicit owner decision (2026-07-17), which is why the membership subcollection — not the user profile — is the source of truth for who belongs to a workspace. Full engineering detail: [self-serve-workspaces-contract.md](../../.claude/memory/contracts/self-serve-workspaces-contract.md).

Five things it adds:

- **Create a workspace (paid-gated).** Any signed-in, email-verified user on a **paid** subscription tier (`pro` / `team` / `enterprise` — the gate's `paidTiers` default, matching `PAID_TIER_ROLES` in [paid-offering.ts](../../src/shared/paid-offering.ts)) can create their own workspace and become its **owner** — a user can belong to several workspaces at once. The **Create workspace** button sits in the header of the section (the **Workspaces** section for a plain member, the **User Management** header for staff) alongside the workspace switcher, and on the empty state when you belong to none. A **free-tier** user is refused with a typed `upgrade_required` code and routed to the **Plan & Usage** screen (Settings → Accounts & AI); other typed refusals are `email_unverified`, `workspace_limit` (you've hit your plan's owned-workspace cap, default 3), `invalid_name` (names are 2–50 characters), and `disabled` (creation turned off in config). Every refusal is humanized — no raw Firebase error reaches the screen.
- **Switch the active workspace.** A workspace **switcher** (a dropdown plus a "＋" create button) in the User Management header lists every workspace you belong to and marks the active one. Switching re-mints your identity token for the new workspace and reloads the scoped data. Switching or creating is _self-initiated_, so it never signs you out of your other devices (no token revoke).
- **Leave a workspace.** A plain member can leave a workspace they belong to (but don't own) directly from **Settings → Workspaces** — each row carries a confirm-gated **Leave** action, so leaving no longer requires hunting through the Team-Chat rail. Leaving the _active_ workspace switches you to another one first so you're never stranded; the sole owner is server-refused (owners delete the workspace instead).
- **Upgrade path (was the "Pricing screen").** The standalone mock **Settings → Pricing** section was **RETIRED on 2026-08-18** in the money-surface consolidation. A free-tier user who tries to create a workspace is now routed to the real **Settings → Accounts & AI → Plan & Usage** storefront — one place to compare plans, upgrade, buy credit, and manage billing (see [plan-billing.md](plan-billing.md)). The backend pricing config (`config/pricing`) and its `getPricingConfig`/`loadPricing` accessors are kept but dormant.
- **Org-admin scoped panel.** A workspace **owner or admin** (a per-workspace role, distinct from the global owner/admin staff role) gets a scoped view of User Management: they can manage _their own workspace's_ roster (invite, remove, change the workspace role) but not global roles, tiers, or other orgs. The full all-users view stays super-admin-only. This is the `manage_org_members` capability — see [entitlements-contract.md](../../.claude/memory/contracts/entitlements-contract.md).

Under the hood the membership doc at `organizations/{orgId}/members/{uid}` is the authority (byte-exact `{uid, role, displayName, photoURL, joinedAt, status}`); `users/{uid}.organizationId` + `orgRole` are just the "active workspace" projection that feeds the verified claims. Invites and domain auto-join **add** a membership rather than moving you between orgs. A one-time admin backfill (`backfillOrgMembers`) promotes every already-assigned user into their org's members subcollection.

### Caveats

- **The full admin console is owner/admin-only** — a plain member sees only the **Workspaces** surface (their workspace list + switcher + Create), never the Users / Access Requests roster or invite tools. The whole section stays invisible on a solo/single-user install where the Global Auth gate is off, and to anyone signed out. This is the headline caveat, not a footnote.
- **The desktop admin console is dev/internal-only** — it is compile-time OFF-SHIPPED from the production build (Electron source is extractable, so the code is _removed_, not merely hidden); global staff manage users from the admin webapp, and only the org-scoped workspace surface ships to customers. See [offship-admin-surfaces-contract.md](../../.claude/memory/contracts/offship-admin-surfaces-contract.md).
- **Cloud-backed** (Firebase Auth + Cloud Functions). Not a local-only feature; it does nothing without the Global Auth gate turned on.
- **Not an in-development / Lab-gated feature** — it's a normal shipped section gated purely by _role_, not by a feature flag.
- **Self-protection is enforced** both in the UI and on the server (you can't remove your own access; last-owner protection applies).

## Related

- [team-chat.md](team-chat.md) — the closest relative: it uses the same Global Auth identity and organization model, and the Workspace tab + Bulk Invite feed its membership
- [account-tier-model-defaults.md](account-tier-model-defaults.md) — the _other_ "tier" (Claude account plan → default model), not to be confused with the subscription tiers here
- [consumer-terms-gate.md](consumer-terms-gate.md) — the Terms/consent gate that pairs with the Global Auth sign-in flow
- Omniscio’s unreleased-feature (“Lab”) gate — the gating mechanism the self-serve workspaces layer rode while in development (now shipped)
