Skip to content
Metric VaultHelp Center
Open app

Managing users

The subscriber directory and the Admins screen: how the lists are built, how to search them, and every mutation you can make with its exact effect.

Last updated 2026-08-06

Summary#

Two screens deal with lists of people. Subscribers is the directory of everyone who has ever used the platform, built by aggregating usage and plan records. Admins is the staff list, and it is the only place where a person's access to the console can be changed. This page explains how each list is assembled, what you can filter and export, and precisely what every mutation does to the database.

Purpose#

The directory answers "who are our users and what shape is the base in". The Admins screen answers "who can act on that base". Keeping them apart matters: nothing you do on the Subscribers screen changes anything, while every button on the Admins screen changes who can reach production data.

To change one customer's plan, usage or suspension state, use the Customers screen instead. See Customer lookup and billing.

Requirements#

  • An active admin identity. See Getting admin access.
  • MONITOR_DB bound, since both lists come from D1 through the worker.
  • The owner role for any mutation on the Admins screen.

Permissions#

ActionRole neededEndpoint
Read the subscriber directory, search, filter, export CSVadmin/api/admin/data
Read the admin listadmin/api/admin/users
Add, suspend, activate, promote, demote or remove an adminowner/api/admin/users/mutate

An admin who calls the mutate endpoint receives 403 {"error":"Only an owner can manage admins."}. The Add an admin card and the per-row buttons stay hidden for that role, driven by the me.role value the list endpoint returns alongside the rows.

Admin console → Main → Subscribers

Admin console → Operations → Admins

Step-by-Step Guide#

Browse the subscriber directory#

  1. Select Subscribers in the Main group. The table loads on first open and on every nav click.
  2. Type into the box labelled Search by email or name.... The filter runs client side over the loaded rows.
  3. Narrow with the plan select (All Plans, Starter, Pro, Agency, Enterprise) or the status select (All Status, Active, Suspended).
  4. Press Export CSV to download the filtered set as subscribers.csv with the columns Email, Name, Plan, Status, Joined.
  5. Press View on any row to jump to the Customers screen with that address already loaded.
Screenshot
The Subscribers screen showing the search box, the plan and status selects, and the table with a View button on each row.

How the directory is assembled#

There is no subscriber table. /api/admin/data reads every row of usage_counters and every row of user_plans, aggregates usage per email across all months, then adds anyone who has a plan but no usage. The name column is the local part of the email address. Suspension state comes from a scan of platform_config for suspended:% keys whose value is 1, so it reflects the same switch the Customers screen flips.

The plan shown for a row is resolved in this order: the user_plans row, then this month's usage_counters.plan, then starter as the last resort. That last fallback is why an unrecognized row can appear as Starter rather than blank.

Note

Note: The table has no pagination. Every subscriber renders. The plan filter also omits free and unlimited, and the plan badge has no color for those two values, so they render in the same grey as Starter. Use the Usage & Activity screen when you need those plans filtered.

Read the admin list#

Select Admins in the Operations group. The table Admin accounts has six columns: Email, Role, Status, Added by, Last seen, Actions. Rows are ordered by creation date ascending, so the seeded owners sit at the top.

  • Status renders green for active and red for anything else.
  • Role renders purple for owner.
  • Last seen is the last_seen_at value, stored in milliseconds and stamped every time that admin's token is successfully resolved. It is a real last-request time, not a last-login time.
  • Added by is seed for addresses the first-run seeding inserted, and the acting owner's email for anyone added through the console.
  • Protected seed accounts show the literal word protected instead of buttons.

If the table is empty it reads No admins.

Add an admin#

  1. In the Add an admin card, enter the address in the field labelled name@example.com.
  2. Choose Admin or Owner. The select defaults to Admin.
  3. Press Add admin. A blank field returns Enter an email.; success shows Done. and the list reloads.

The write is an upsert. An existing row for that address has its role replaced and its status forced back to active, which makes Add an admin the fastest way to un-suspend someone as well as the way to create them.

Change an existing admin#

Each unprotected row carries up to three buttons.

ButtonWhat it sendsEffect
Suspendupdate with status: "suspended"The row stays but the address is denied at sign-in, even if it is on the seed floor
Activateupdate with status: "active"Restores access with the existing role
Make ownerupdate with role: "owner"Grants every mutation right
Make adminupdate with role: "admin"Reduces the account to read-only across the console
RemoveremoveDeletes the row outright

Every mutate action in full#

POST /api/admin/users/mutate takes { token, action, email, role?, status? }. The address is trimmed and lower-cased, and must contain an @ at position 1 or later, otherwise the call returns 400 {"error":"A valid email is required."}.

ActionAccepted extrasDatabase effectGuardsAudit entry
addrole (owner accepted, anything else becomes admin)INSERT ... ON CONFLICT(email) DO UPDATE SET role = excluded.role, status = 'active', recording added_by and created_atA seed address is forced to owner regardless of the role sentadmin.add, meta role=<role>
updaterole (owner or admin only), status (active or suspended only)UPDATE admin_users SET role = COALESCE(?, role), status = COALESCE(?, status)Suspending or demoting yourself returns 400 {"error":"You cannot suspend or demote your own account."}. Suspending or demoting a seed address returns 400 {"error":"This is a protected owner account."}admin.update, meta `role=<r-> status=<s->`
removenoneDELETE FROM admin_users WHERE email = ?Removing yourself returns 400 {"error":"You cannot remove your own account."}. Removing a seed address returns 400 {"error":"This is a protected owner account."}admin.remove
anything elsenonenone400 {"error":"Unknown action."}none

Values outside the accepted sets are silently ignored rather than rejected. An update carrying role: "superuser" passes null into the COALESCE, so the role is left unchanged and the call still returns {ok:true}. Check the row in the table after any update rather than trusting the success message alone.

Screenshot
The Admin accounts table showing an owner row marked protected and an admin row with Suspend, Make owner and Remove buttons.

Undoing a change#

ChangeHow to undo
Suspended an adminPress Activate, or re-add the address, which forces status back to active
Demoted an adminPress Make owner
Removed an adminRe-add the address. The row is recreated, but created_at and last_seen_at start again and added_by becomes you
Added the wrong addressPress Remove

Nothing on either screen notifies the person affected. There is no invitation email for admin access; you tell them out of band.

Troubleshooting#

SymptomLikely causeFix
No buttons on any admin row, and no Add an admin cardYour role is adminAn owner promotes you with Make owner
A row shows protected and cannot be changedThe address is on the seed floor, either hardcoded or in ADMIN_EMAILSRemove it from ADMIN_EMAILS and redeploy if it really must be demoted
This is a protected owner account.Same cause, on a suspend, demote or removeAs above
You cannot suspend or demote your own account.The self-action guardAsk another owner to make the change
A newly added admin still cannot sign inTheir Supabase user does not exist or the email is unconfirmedConfirm the address in Supabase, then retry. See Getting admin access
Last seen is blank for an active adminThey have not made an authenticated request since the column started being stampedNot an error; it fills on their next request
A subscriber is missing from the directoryThey have neither a user_plans row nor any usage_counters rowLook them up directly on the Customers screen
A subscriber shows the wrong planTheir plan is resolved by code override, not by the stored rowCheck DEV_UNLIMITED_EMAILS and FORCE_FREE_EMAILS in the worker

FAQs#

Does removing an admin sign them out? Not immediately. Their existing Supabase access token remains valid until it expires, though every admin endpoint will reject it as soon as the row is gone, because the token is re-resolved on each request.

Why does the directory show more people than we have paying customers? It counts anyone with a usage or plan record. The Paying customers figure on the Usage & Activity screen is the one that excludes Starter.

Can I change a customer's plan from the Subscribers screen? No. Subscribers is read-only. Press View to open the same customer on the Customers screen, where the plan control lives.

Is there an export of the admin list? No. CSV export exists for Subscribers, Usage, Run Log and the audit log, not for the admin table. The audit log records every admin change if you need a history.

Why did an update return success but change nothing? Because unrecognized role or status values are coalesced away rather than rejected. Re-read the row to confirm.

See also

Was this article helpful?