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 auth: enabled. 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 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 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 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
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
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.