# Manage your end-users

The management App is where you and your support team work with the directory: find an end-user, read what Lessly Users knows about them, and act — invite, edit, ban, revoke a session, reset a lost second factor, delete or erase an account. Everything here is scoped to one product and needs no code on your side.

Every operation below is available to an agent as well, so a support workflow can be automated once you have done it by hand and know what it does.

## Find a user

### UI

In the management App (`app.lessly.com`), open **Users**. It opens on a list of your end-users, newest first. Search, or narrow it with the status filter.

### MCP

Ask your agent:

```text
Find the Acme end-user with the id usr_7Kq2mZ1xR4.
```

The agent needs to be allowed to read your product's directory.

Search is one box, and it matches a substring of an email address or of the user's name, case-insensitively. It does not match the user id: to land on a record from an id you hold in your own logs or in a token, read that user directly with `users_users_get` or go to its address in the App.

One filter narrows the list without a search term: **status**, as **All**, **Active**, **Banned** or **Waitlisted**. There is no filter by sign-in method, by verification state or by date, on either surface — `users_users_list` takes the search term and the status, and nothing else that narrows.

People who signed up to a waitlist are directory records too, and they are kept out of the default list so they do not drown the users who can actually sign in. The **Waitlisted** filter is what brings them back, or work with them in the funnel described in [Run a waitlist](/ship/users/waitlist). Soft-deleted users are hidden from the list the same way, and the App offers no control that shows them; `users_users_list` has `includeDeleted` for that.

## Read a record

Opening a row from **Users** opens that user's record. It carries six panels — Identifiers, Profile, Impersonation, Sessions, MFA factors and Audit trail — and the ban, delete and erase buttons sit in its header.

**Identity.** The stable opaque id — the one you store in your own database and the one that appears in every access token and event — plus the name and image if your product collects them.

**Identifiers.** The email addresses that reach this person. A user may hold several; exactly one is primary. Each address carries its own verification state, so a record can hold a verified address and an unverified one at the same time. "Is this user verified?" is always a question about an address. The panel lists them and marks the primary one; the only action on it is **Set primary**, described below.

**Sign-in methods.** Separately from the addresses, the ways this person can prove who they are: a password, a linked Google or GitHub account. The record shows neither. There is no panel for a linked provider account, no way to unlink one, and no way to clear a password — those are not operations this product offers on either surface today.

**Sessions.** One row per session, naming the device by what it reported and the address it connected from, with when it last refreshed and when it expires. Ended sessions stay in the list, marked as ended and without a revoke button, because a support case needs what happened and not only what is live.

**Factors.** Enrolled multi-factor methods: an authenticator app once its enrolment has been confirmed, and how many single-use backup codes remain.

**Profile and metadata.** Three bags of your own data hang off the record, and they differ in who may write them and who may read them.

| Bag | Written by | Read by | Notes |
|---|---|---|---|
| Public metadata | you, from your backend or an agent | your backend and the end-user | A size-capped projection of it travels in the access token |
| Private metadata | you, from your backend or an agent | your backend | Never leaves your side |
| Unsafe metadata | the end-user | everyone | Never base an authorization decision on it |

Use public metadata for anything your backend wants without a lookup — a plan name, a role, a feature flag. Because it rides in the token, it is capped, so an over-large write is refused rather than silently breaking that user's next sign-in.

> **NOTE**
> **No screen writes metadata.** The record's Profile panel sends the display name, the avatar URL, the waitlist flag and the must-reset-password flag, and nothing else. All three bags are written through `users_users_update`, from your backend or an agent, and a write replaces the whole bag rather than merging into it — send the object you want stored.

**Security history.** The record's own audit trail, described at the end of this page.

## Create and invite a user

These are two acts, and the App keeps them apart. Creating writes the record and mails nobody. Inviting writes the record too — the person appears in the directory at once, with an unverified address and no credential — and then mails a one-time link that sets their first password and verifies the address. That link is how invite-only sign-up works, and accepting it creates no session by itself.

### UI

**Users → Invite user** takes the address and an optional name, and mails the invitation. **Users → Create user**, next to it, writes the record without mailing anyone; it takes the address, an optional name, and a switch that records the address as already verified. Neither dialog takes metadata — `users_users_create` and `users_users_invite` do.

### MCP

Ask your agent:

```text
Invite ada@example.com to Acme as an end-user.
```

The agent needs to be allowed to create and invite users in your product's directory.

An invitation is single-use and expires; its lifetime and the cooldown on re-inviting the same address are configuration fields (`invite`), with no screen. Inviting is idempotent by address: re-inviting returns the same user rather than creating a second, and whether it mails again is a cooldown decision, so pressing the button twice cannot flood a mailbox. Nothing is mailed at all until this product has a verified sender address. Neither surface revokes an invitation, so an invitation you regret is one you wait out or ban the record for. Accepting it applies everything your product's configuration requires — a password that meets the policy, a second factor if you demand one.

## Edit a record

**The profile** is the Profile panel on the record: the display name, the avatar URL, the waitlist flag and the must-reset-password flag. It is a form with an explicit **Save profile**, and it sends only the boxes you changed, so a value someone else changed while you had the record open is not clobbered by your save. Emptying a text box clears that field rather than leaving it alone.

**Metadata** is not on that form, or on any other. Write the bags with `users_users_update`. A change to public metadata reaches your backend on the user's next access token, within the token lifetime.

**The primary identifier** is changed from **Identifiers → Set primary**, and it is not a field edit. It opens a dialog that asks for the new address and for a written reason — what identity proofing you did, under which ticket — and the reason is recorded in the audit row word for word. The swap is then scheduled rather than applied: the panel answers with a request id and the moment it takes effect, the old address is mailed immediately, and the delay is this product's `emailChange.overrideDelayHours`, 72 hours by default. The primary address is what Lessly Users uses to reach the person and what your backend receives in the token's email claim.

**Adding or removing an address** has no screen and no tool. A record's addresses are the ones it was created with; plan for that when you import.

### UI

Open the record from **Users**, edit the Profile panel, and press **Save profile**. **Identifiers → Set primary** is the separate, delayed request described above.

### MCP

Ask your agent:

```text
Set the public metadata plan to pro for the Acme user usr_7Kq2mZ1xR4.
```

The agent needs to be allowed to change records in your product's directory.

> **WARNING**
> If an end-user cannot complete an email change themselves because the old address is dead — an imported account, a closed mailbox — an operator can complete it for them. That override asks you to confirm you have identified the person, is recorded, takes effect after a delay, and notifies both addresses in the meantime, so a mistaken or malicious change can be stopped.

## Reset a lost second factor

When a user loses both their authenticator app and their backup codes, an operator resets their factors from the record. The reset clears the enrolled factors, revokes all of the user's sessions, and lets them enrol again on their next sign-in.

### UI

On the record, **MFA factors → Reset MFA**. The dialog will not submit without a written reason, and its button says **Schedule reset** because nothing is removed at the moment you press it.

### MCP

Ask your agent:

```text
Reset the second factor for the Acme user usr_7Kq2mZ1xR4 — I have verified their identity.
```

The agent needs to be allowed to reset factors in your product's directory.

> **WARNING**
> A support-desk reset is exactly what an attacker would ask for, so it is deliberately not instant: it asks you to confirm you have identified the person, it is recorded with your name against it, it takes effect after a delay, and the user is emailed with a way to stop it during that window.

## Revoke a session

From the sessions list you can end one session — that device signs out and nothing else changes — or end all of them at once. A revoked session's refresh stops working immediately.

### UI

On the record, the **Sessions** panel lists live and ended sessions together. **Revoke** sits on each live row, **Revoke all** at the head of the panel.

### MCP

Ask your agent:

```text
Sign the Acme user usr_7Kq2mZ1xR4 out of every device.
```

The agent needs to be allowed to revoke sessions in your product's directory.

Because your backend verifies access tokens locally, a revoked session's current access token remains valid for the rest of its lifetime — ten minutes by default. Where that matters, the server library can check a token against the live session on the calls that deserve it; [Sessions and tokens](/ship/users/sessions-and-tokens) covers both modes, and [Configure authentication](/ship/users/configuration) covers shortening the token lifetime.

## Ban and unban

Banning refuses every future sign-in and revokes all existing sessions at once. The record, its identifiers and its metadata stay exactly as they were, so a ban is reversible: unbanning restores the account, and the person signs in again with the credentials they already had.

Use a ban for abuse and for accounts you may want back; use deletion for accounts that are leaving.

### UI

**Ban** is in the record's header, and turns into **Unban** once the account is banned. The **Users** list carries the same pair on each row, so a ban does not need the record open.

### MCP

Ask your agent:

```text
Ban the Acme user usr_7Kq2mZ1xR4.
```

The agent needs to be allowed to ban users in your product's directory.

> **NOTE**
> A ban is not the automatic lockout after repeated failed sign-ins. A lockout is temporary, expires by itself, and the user is emailed a way to lift it sooner; it is not something you have to clear by hand.

## Impersonate a user for support

To reproduce what a user is seeing, you can open a session as them from the **Impersonation** panel on their record. It is the strongest thing the App can do, and it is fenced accordingly:

- the button is disabled until your product's redirect URI allowlist has an entry in it, because an impersonated session has nowhere legal to land otherwise;
- the dialog will not submit without a written reason, and that reason is both recorded and quoted to the user in the email they receive;
- the session is capped at thirty minutes and cannot be refreshed;
- nothing about the account can be changed while you are inside it — no password, no factors, no addresses, no session revocations;
- your backend can tell: the session is marked as an impersonation and the server library exposes it as such, so you can hide sensitive screens or refuse destructive actions during one;
- it is recorded with your name as the actor, and — unless you turn the notice off for your product — the user is emailed that it happened.

**What the App hands back is an access token, not a signed-in browser tab.** The panel shows the session id, the moment it expires and the token itself, with a **Copy access token** button. Nothing redirects, and no session is transferred to the tab you are standing in. Your product's own integration is what consumes the token, over whatever route it already has. There is no refresh token, so when the thirty minutes are up the session is over.

## Delete or erase an account

Two different operations, for two different reasons.

| | Delete | Erase |
|---|---|---|
| What it is for | An account that is leaving. | A data-protection request. |
| Sessions | Revoked at once. | Revoked at once. |
| The record | Kept for a grace period, so a mistaken deletion can be undone and your own systems can reconcile. | Purged: addresses, profile, metadata and credentials are gone; a tombstone stands in for the identity. |
| The email address | Released for re-use after the grace period. | Free again immediately. |
| Undo | Yes, during the grace period. | None. Erasure is final. |
| Security-log entries | Kept. | Kept, with the identity in them pseudonymized. |

### UI

**Delete** and **Erase** are both in the record's header, deliberately styled apart, and each asks for confirmation naming what it does.

### MCP

Ask your agent:

```text
Erase the Acme user usr_7Kq2mZ1xR4 — they filed a deletion request.
```

The agent needs to be allowed to delete and erase users in your product's directory.

> **DANGER**
> After an erasure the address is free again. If the same person signs up later with the same address they get a **new** user id — a deliberate consequence of erasure, and one more reason to key your own tables by the id rather than the email. Before either operation, export: **Users → Export** writes the list as it is filtered on screen, and there is no per-user export button. Note that no export carries password hashes. See [Migrate users in and out](/ship/users/migration).

What erasure means for the data itself, and how it fits the platform's retention and deletion model, is on [Data and privacy](/privacy/data-and-privacy).

## Import users in bulk

Bringing an existing user base with you is a first-class operation in the App, not a support ticket. **Import** takes the file in three steps: paste or upload the rows, say which column is which, then read what will happen before anything is sent. The screen maps four columns — the email address, a display name, the password hash you hold today, and the algorithm that hash belongs to — and its preview counts the rows it will send and the rows it rejected, each rejection named with its row number and reason.

What happens on the run:

- each accepted row becomes a directory record with its address in the verification state you declared;
- a supplied password hash is stored as it is, and the password keeps working — it is verified in its original format the first time that person signs in, then transparently re-stored in argon2id;
- a hash that cannot be used marks the account as needing a password reset, so the user is asked to set a new password rather than being locked out;
- rows with no credential at all become invitable accounts, which you then invite from the App as above.

The whole import is recorded in the security history. [Migrate users in and out](/ship/users/migration) has the file format, the supported hash formats, and the export path back out.

## Read the audit trail

Two records are kept, for two audiences.

| | The user's security history | The product-wide log |
|---|---|---|
| Organized by | whom it was done to | when it happened |
| Where you read it | the **Audit trail** panel on the record | `users_audit_list`, which has no screen |
| The question it answers | "what has happened to this account?" | "what was done in this product last Tuesday?" |
| Covers agent actions | yes | yes |

Both read the same log; the record's panel is that log filtered to one user. Entries are append-only: nothing in the App edits or removes one.

Every entry carries the actor, the action, what it targeted and when it happened. Note what the filters do not offer: `users_audit_list` narrows by action, by target and by time range, and there is no way to ask it what one member of your team did — an actor is something you read off an entry, not something you can filter for. Together with the notification emails Lessly Users sends on its own — a changed password, a new device, a requested email change with its revert link, a pending factor reset — this is what makes an operator override safe to offer: nothing sensitive happens quietly, either to your team or to your end-user.

## Next steps

- [Configure authentication](/ship/users/configuration): the sign-in methods, password policy and session lifetimes the operations above obey.
- [Read the token contract](/ship/users/sessions-and-tokens): why a revoked session can outlive its revocation by one token lifetime, and how to close that gap.
- [Migrate users in and out](/ship/users/migration): the import file format, and getting your users back out.
- [Run a waitlist](/ship/users/waitlist): the directory before your product is open to everyone.
