Problem
The console's admin side shows people and accounts as two flat lists. The People page answers four things per person: the subject, the address, the platform role and when we last saw them. Anything else means a database query or the identity provider's own admin interface:
- whether their address is verified, whether a second factor is set up, how they sign in;
- which accounts and teams they belong to, and what those use;
- whether they asked for access, which agreements they were sent and which they signed;
- whether an erasure is scheduled for them.
The service already read the whole identity for each row and kept only the role.
There was also no supported way to act on a person. Signing someone out, sending them the partner agreement without an access request, or starting an erasure for a person who asked by mail meant work outside the platform. Nothing recorded who did it or why.
Proposal
The directory
ListPeople answers more per person, still from one identity-provider read each:
- their name and when the identity was made;
- whether the address is verified and a second factor is set up;
- the kinds of sign-in they use (
password,oidc:google,passkey, ...); - how many accounts they belong to.
A new role filter narrows the list to one platform role. Roles live with the
identity provider, so the filter reads the list in chunks and keeps the matches. A
page reads at most 200 people and may come back short. The caller follows
next_page_token until it is empty.
A person's page
GetPerson gathers, for one subject:
- their accounts, with their role in each and when they joined;
- their own account's units this month;
- their access request, the agreements sent to their address and their signatures (version, the role it gives, state, dates);
- active sign-in sessions and the latest consent per purpose;
- whether an erasure is scheduled, and when it runs;
- their role changes and the admin actions on them.
A signature shows its state and dates, never the typed name or the answers.
An account's page
GetAccountAdmin does the same for one account:
- its owner and members, and its open invitations;
- its keys and API tokens, by name and dates only;
- this month's units, split by category, and how many domains it added;
- its last 50 log entries and its unit changes;
- the admin actions on it, and whether an erasure is scheduled.
A key's secret, its hash and even the last four characters of the secret stay out of the answer.
Actions
- Sign out. The identity provider ends every sign-in session of the person, then the token issuer ends their consent and login sessions, so refresh and access tokens stop. If the first step fails, the second is not tried and the admin sees the failure.
- Make a partner. The published agreement goes out as an invitation that gives
partneronce signed, the same path an approved access request takes (RFC 0057). An undecided request of theirs is approved with it. The admin chooses who signs: an individual (the person themselves, by their name) or a company (its registered name). A partner need not be a company (owner, 2026-10-07). The choice goes into the request asparty_kindand is recorded as the kind of the agreement's party, so the invitation, the agreement's party block, the signature and the admin's lists all carry it. Without a kind the service infers it as before: a company when one is named or asked for, else the person. Change role listspartnertoo, and choosing it opens the same dialog: the role comes only from a signed agreement, never by hand. A person who is a partner already is refused.
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
- Erasure of a person. The grace-period erasure a person can start for themselves
(
DeleteAccount), started by an admin with a required reason, and cancelled inside the grace window. Teams they own alone go with them; a team they own with other members blocks it, as it does for the person. - Erasure of a team. The team deletion with its grace window, without the admin being a member, and its restore. A personal account goes through the person's erasure instead.
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
An admin cannot sign out, erase or restore themselves from these pages; their own account page does that.
Audit
Every action writes one row of a new table, accounts.admin_actions: when, which admin,
the action, the target (a person's subject or an account's id) and the admin's reason. The
table refuses updates. Every action, and every read of a person's or account's page, also
writes an audit line with who, what, the target and the outcome. Neither carries the
reason, an address or anything the person wrote. The pages show the rows newest first.
Security and compliance
A diagram is drawn here in the RFCs product; this page does not show diagrams yet.
- Admin only, twice. Every route is under
/v1/accounts/admin/, which the edge already admits only for the platform's admin role, and the service checks the role again before it reads anything. Any other caller, including a team's own owner, getsPERMISSION_DENIED. - Least data. The pages show what an admin needs to support a customer: states,
counts, dates and names. No secret, token, credential identifier, signature text or
answer leaves the service. Sign-in kinds name the provider (
oidc:google), never the account at the provider. A reason travels in a request body, never in a URL: cancelling an erasure is a POST, not a DELETE with a query string. - Accountability. Every action is a database row and an audit line, so a review can answer who signed a person out, who started an erasure and why. The rows outlive the person's erasure as the record that the action was taken; the target is a pseudonymous id that stops resolving.
- Partner stays agreement-gated. Making a partner sends the agreement and nothing
else. The role still comes only from a signature (RFC 0056, RFC 0057); an admin cannot
set
partnerby hand. - Erasure keeps its grace period. An admin starts the same erasure a person can start for themselves, with the same grace window and the same sweeper. Nothing is erased at once, and a mistake can be undone inside the window.
- Reads of personal data are logged. Opening a person's page is an audit line with the admin and the subject, so access to a customer's data has a trail.
Alternatives
- The identity provider's own admin interface. It has the identities but none of the accounts, agreements, units or erasures, and no record of why something was done. It would also put a second admin surface on the internet.
- A role column in our database for filtering. Faster filters, but a second copy of the role that can drift from the one the tokens carry. The chunked scan is slower and always right; at the platform's size it is enough.
- Setting
partnerby hand. Simpler, and it would skip the agreement the role exists to require.
Open questions
- Retention of
accounts.admin_actions: the audit period is one year at least. A job that deletes older rows is not built yet. - Whether a person's data export should list the admin actions taken on them.
Status log
- 2026-10-06: opened. Built in the accounts service: the richer
ListPeoplewith the role filter,GetPerson,SignOutPerson,MakePartner,StartPersonErasure,CancelPersonErasure,GetAccountAdmin,DeleteTeamAdmin,RestoreTeamAdmin, theadmin_actionstable and its audit lines, with integration tests against a real database and mocked identity provider and token issuer. Next: the console's People pages, then its Accounts pages. - 2026-10-06: the console's People pages. The directory reads every person once and filters by role, sign-in and a search, with counts of who joined and who was active in the last week, who holds a role from an agreement and the open access requests. A person's page shows their profile, accounts and units, access request, agreements, what admins did to them, consent and erasure, and asks for a reason before every action.
- 2026-10-06: the console's Accounts pages. The list reads every account once and filters by kind, plan, state and a search, with counts of accounts, teams, paid plans, probes and deletions. An account's page shows its people, open invitations, keys (names and dates, never a secret), this month's units by category and its log, and holds the plan move, grants, the probe mark and a team's deletion and restore. Subscriptions and invoices wait for the billing service's admin reads; the Billing tab says so.
- 2026-10-06: live. The service side since accounts and protocol at 27004b08, the console's People and Accounts pages since console-ui at 738486ad. The Billing tab now shows the account's subscription (plan, seats, status, test or live mode, period end, the last failed payment) and its invoices with links to Stripe's own pages, read through the billing service's admin reads (#465); it ships with the next console roll.
- 2026-10-07: Decided: built and live (#390 in accounts, #465 in console-ui).
- 2026-10-07: a partner may be an individual (owner decision).
MakePartnerRequest.party_kind(individual or company), the access-request path honours it, andNdaInvitationandPersonSignaturecarry the kind for the person's page and the NDA lists. The dialog asks who signs, the company's name only for a company, the country from a picker, the language, the reason, and says in one line what will happen. Change role listspartnerand hands over to the same dialog. Audit lines carry the kind and the outcome, never a name or an address.