Users and passwords
Create and manage a realm's users with backd user or the admin API.
The backd user commands manage the users of a realm with an auth section. They call the realm’s admin API on a running backd, so you first log in as an administrator (or set BACKD_API_KEY to an admin API key). The realm’s first administrator is created with backd-server bootstrap. Server-side services can call the admin API directly.
backd login --realm blog --url https://api.example.com
backd user list --realm blog
Every command also takes --url, to pick the server for that command.
Commands
| Command | Effect |
|---|---|
backd user create --realm <realm> --email <email> | Create a user (with a verified address) and ask for their password |
backd user create --realm <realm> --email <email> --no-password | Create a user without a password; they can’t sign in with one until it’s set |
backd user list --realm <realm> | List the realm’s users |
backd user set-password --realm <realm> --email <email> | Set or replace the password, and revoke all of the user’s sessions |
backd user expire-password --realm <realm> --email <email> [--reason <text>] | Expire the password: it stops opening the account until the user resets it; sessions are kept |
backd user expire-passwords --realm <realm> --yes [--reason <text>] [--keep-sessions] | Expire every password of the realm and end every session: the first step after a leak |
backd user notify-expired --realm <realm> | Email every user whose password was expired a link to choose a new one (also expire-passwords --notify) |
backd user reset-link --realm <realm> --email <email> [--redirect-to <url>] | Print a one-time reset link for a user the email can’t reach; nothing is sent |
backd user revoke-sessions --realm <realm> --all --yes | End every session of the realm and leave passwords alone |
backd user change-email --realm <realm> --email <email> --new-email <email> | Change the user’s address at once, as an administrator (needs email; see the admin API) |
backd user identities --realm <realm> --email <email> | List the user’s ways to sign in: a password and the providers they use, with the address each reported and when it was linked and last used |
backd user mfa-reset --realm <realm> --email <email> --yes | Remove a user’s second factor and recovery codes and end their sessions; backd-server user mfa-reset is the break-glass form that acts on the database |
backd user mfa-status --realm <realm> [--email <email>] | A user’s second factor, or without --email the users of the roles that must use one and have none yet |
backd user unlink-identity --realm <realm> --email <email> --provider <name> | Remove one of them (password, google, …); never the last one |
backd user owned --realm <realm> --email <email> | Show what erasing the user would do, per collection that declares a policy |
backd user verify-email --realm <realm> --email <email> | Mark the email as verified |
backd user disable --realm <realm> --email <email> | Block sign-in and revoke all of the user’s sessions, without deleting the account |
backd user enable --realm <realm> --email <email> | Let a disabled user sign in again |
backd user delete --realm <realm> --email <email> --yes | Erase the user: a tombstone, and the collections’ policies applied; waits for the job (--wait) and prints the counts. Irreversible: see Deleting and erasing users (disable keeps the data) |
backd user add-role --realm <realm> --email <email> --role <role> | Assign a role declared in realm.yaml |
backd user remove-role --realm <realm> --email <email> --role <role> | Take a role away |
backd user networks --realm <realm> --email <email> | Show the user’s network restrictions |
backd user networks --realm <realm> --email <email> --admin <list> --login <list> | Set them: comma-separated IP addresses or CIDR networks, or none to lift one. A list you don’t give is kept |
Example:
backd user create --realm demo --email ada@example.com
# Password:
# Repeat password:
# created user ada@example.com (id d3c9ljp8hc2g00b6s1m0)
backd user list --realm demo
# EMAIL ID VERIFIED DISABLED ROLES CREATED
# ada@example.com d3c9ljp8hc2g00b6s1m0 false false 2026-09-26T12:00:00Z
On a terminal, passwords are read twice without echo. Otherwise, the first line of standard input is the password, which suits scripts:
printf '%s\n' "$NEW_PASSWORD" | backd user set-password --realm demo --email ada@example.com
Expiring a password
backd user expire-password makes a user’s current password useless without changing it or touching their sessions, for a support case or a password you no longer trust. The user’s next sign-in with that password is refused with 401 password_expired (a wrong password is still invalid_credentials, so the answer tells nothing to someone who doesn’t know it), and when the realm sends email a reset email is queued, as if they had asked for one. Choosing a new password through the reset, or an administrator running set-password, makes the account usable again; the user’s expiry is cleared by the new password (the list and the user’s page stop marking it). The action is audited as password.expire, with the optional --reason.
Users who sign in with a provider or a magic link have no password to expire, and an expired password does not skip or start the second factor: it is refused first. The user can choose a new password themselves, signed out, with POST /_auth/password/update (the current password, and a code if they have a second factor); a password expired for the whole realm cannot. The same action is POST /_admin/users/{id}/expire-password in the admin API.
Expiring every password
After a leak of a backup or a credential dump, backd user expire-passwords --realm blog --yes --reason "leaked backup" makes every existing password useless in one step. It sets the realm’s password epoch to now: a password set before it stops opening its account (401 password_expired for the right password, with the reset email described above), and every session ends. --keep-sessions leaves the sessions, for a planned rotation rather than a breach. The answer says how many accounts have a password and how many sessions ended.
- One write, whatever the number of users. A user created or a password reset after it is valid; restoring a backup of older system data brings the older epoch back, so run the command again after such a restore.
- It only moves forward, and cannot be undone through the API; a user recovers with a reset, or you set a password with
set-password. - It needs full administration and a fresh proof of identity (the CLI asks for it), and is audited as
password.expirewith the reason and the counts. - The whole response to a leak, in order and with what users will see, is in If credentials leak.
backd user revoke-sessions --all --yesends every session, and the devices they registered, without touching passwords. It is audited assession.revoke_all.
The admin API calls are POST /_admin/passwords/expire ({"reason"?, "keep_sessions"?}) and DELETE /_admin/sessions.
Telling the users
A user whose password stopped working finds out by trying it, and that sign-in sends them the password-expired email with a link. To tell everyone without waiting, add --notify ("notify": true in the API): backd user expire-passwords --realm blog --yes --notify, or later backd user notify-expired --realm blog. A job then walks the users with a password and queues one email each through the normal email queue:
- Who: enabled users with a verified address whose password the epoch expired and who were not told after it. A user created or reset since is not selected; users who only use providers or magic links have no password to expire.
- Once each: a user is marked as told when their message is queued, so repeating, cancelling and starting again, or a worker that dies and is replaced never writes to anyone twice.
notify-expiredgoes on from where it was. - Within the email limits: the limits apply as for any message. Users they hold back are told after a wait (up to an hour), not failed, and the job reports how many.
- Following it: the job is
backd:password.notify;backd functions jobslists it with its progress, andbackd functions cancelstops it (cancelling the job does not undo what was queued). It ends audited aspassword.expire_notifywith the counts. - Without email: a realm that sends no email refuses
--notifybefore expiring anything; give each user a link yourself instead.
A reset link without email
When a user can’t be reached by email (the mailbox is lost or never worked), backd user reset-link --realm blog --email ada@example.com prints a one-time link to choose a new password, and sends nothing:
backd user reset-link --realm blog --email ada@example.com
# https://api.example.com/v1/blog/_auth/reset-password?token=…
# reset link for ada@example.com, shown once; it works once and expires at 2026-10-22T20:15:00Z. …
- It is the link a reset email carries. Opening it shows the hosted form; using it sets the password, ends the user’s sessions, tells them with
password-changedand uses the token up. Unused, it expires like a reset token (account.tokens.reset_password, an hour by default).--redirect-tomust be within the realm’semail.allowed_redirects, like a reset’s. - Hand it over by a channel you trust. Whoever holds the link can take the account, so it is as strong as the channel you use. It is shown once; make another if it is lost.
- Needs the realm’s
emailsection (it serves the form and knows the public address) and full administration with a fresh proof of identity. An administrator can’t make one for a user with rights they lack, nor for a disabled user. - Audited as
password.reset_linkwith the administrator and the user, never the token. The admin API call isPOST /_admin/users/{id}/reset-link.
Roles
Roles are declared in realm.yaml, which can also assign them to users by email. backd user add-role and backd user remove-role change assignments in the database:
backd user add-role --realm demo --email ada@example.com --role editor
# role "editor" assigned to ada@example.com
# warning: this assignment is stored only in the database. If the realm is rebuilt from its config, it won't come back.
# To keep it, list ada@example.com under roles.editor.users in demo/realm.yaml.
- Only roles declared in
realm.yamlcan be assigned. - These warnings need the realm’s
realm.yaml, which the CLI reads fromCONFIG_DIRwhen it is set (as it is inside thebackdcontainer). Elsewhere, the CLI prints a general note instead. - Changes apply from the user’s next request. There’s no need to sign in again.
- Removing your own last admin role ends your access to the admin API; see Command-line administration.
Network restrictions
backd user networks --realm demo --email ada@example.com --login 10.20.0.0/16
# ada@example.com
# admin networks: any
# login networks: 10.20.0.0/16
Settings made here live only in the database. If realm.yaml sets networks for the same email, they win at the next start, and the CLI warns about it (with CONFIG_DIR, as for roles). Admin networks must lie within the realm’s admin.allowed_networks.
Emails
- Emails are trimmed and lowercased, so
Ada@Example.comandada@example.comare the same user. - Each email belongs to at most one user in a realm.
- Addresses on the reserved
.invaliddomain are refused (erased users’ placeholders live there), and so are addresses with a comma, semicolon, colon, angle bracket, parenthesis, square bracket, quote, backslash or control character: a delivery function that joins recipients into one header must never get one address that is really two. - An address changes only in guarded ways: the user asks with their password and confirms through a link sent to the new address (off unless the realm sets
account.allow_email_change, see Sessions), or an administrator does it through the admin API, which tells both addresses and offers the old one an undo link.
Password policy
- At least
auth.password.min_lengthcharacters (default 12, set inrealm.yaml), at most 128. - Lengths count characters, not bytes, and there are no rules about character classes. Long passphrases are encouraged.
- Passwords are normalized to Unicode NFKC before hashing, so the same text typed on different keyboards or systems matches.
Password storage
Passwords are hashed with argon2id (64 MiB of memory, 3 passes, a random 16-byte salt per password) and stored as standard PHC strings such as $argon2id$v=19$m=65536,t=3,p=1$…. The parameters are stored with each hash, so they can be raised later without breaking existing passwords.
Each hash takes about 64 MiB of memory. To keep memory bounded when many passwords are hashed at once, each backd process runs at most PASSWORD_HASH_CONCURRENCY hashes at a time. By default that is the number of processors Go uses (which follows a container’s CPU limit), and never more than fit in half the memory limit (the container’s, or GOMEMLIMIT): a 4 CPU, 512 MiB container hashes at most 4 passwords at once (256 MiB / 64 MiB), and a 1 CPU container one at a time. The chosen value is logged at startup ("msg":"password hashing"). Further requests wait for a free slot; if their deadline passes first, they fail with a “try again later” error instead of exhausting memory.