Realm settings (realm.yaml)
Configure authentication, sign-up, sessions, passwords, CORS and roles for a realm.
Every realm directory must contain a realm.yaml. It holds the settings that apply to the whole realm: its users, how they sign in, and which browser origins may call it.
$CONFIG_DIR/<realm>/realm.yaml
If a realm has no realm.yaml, backd refuses to start and names the realm. Create a commented starter file with:
CONFIG_DIR=./config backd template realm --realm <realm>
Example
auth: enabled
signup: invite
sessions:
idle_timeout: 14d
max_lifetime: 60d
password:
min_length: 14
cors:
origins:
- https://app.example.com
- http://localhost:5173
roles:
admin:
description: Manages users and invitations
admin: true
users:
- ops@example.com
- email: lead@example.com
editor:
description: Can publish articles
An empty file, or one with only comments, is valid: every key has a default.
Keys
| Key | Default | Meaning |
|---|---|---|
auth | enabled | enabled or disabled. With enabled, data routes need credentials (see who can access data) and provisioning creates the realm’s system database. A realm with disabled has no users, keys or access rules, so anyone who can reach backd can read and modify its data; backd logs a warning for it at startup. |
signup | closed | Who can create accounts: open (anyone), invite (needs an invitation), closed (only operators, with the CLI or the admin API). |
sessions.idle_timeout | 30d | A session expires after this long without use. |
sessions.max_lifetime | 90d | A session never lives longer than this. Must be at least idle_timeout. |
sessions.cookie.enabled | false | true: browser apps can keep a session in an HttpOnly cookie, when their login asks for one. Needs explicit cors.origins (no *). See Session cookies |
sessions.cookie.same_site | lax | lax, strict or none (an app on another site than the API) |
sessions.admin_idle_timeout | idle_timeout | The idle timeout for users who hold an admin role. Can only shorten idle_timeout. |
sessions.admin_max_lifetime | max_lifetime | The longest an admin’s session lives. Can only shorten max_lifetime; must be at least admin_idle_timeout. |
password.min_length | 12 | Minimum password length, from 8 to 128. |
login_throttle.account_threshold | 5 | Failed logins an email address gets before each new attempt must wait (1 or more). See Brute-force protection. |
login_throttle.ip_threshold | 50 | The same per client address (an IPv6 /64), higher because a network can hide many people. |
login_throttle.window | 15m | How long a failure counts: a counter expires this long after its last failure. At least 1m. |
login_throttle.max_delay | 15m | The longest anyone waits; the wait starts at 1 second and doubles with each failure. At least 1s, and not more than window. |
cors.origins | none (CORS off) | Browser origins allowed to call this realm, as scheme://host[:port]. * allows any origin and must be the only entry. See CORS. |
roles | none | Roles that access rules can check. See below. |
openapi.public | false | true serves the realm’s OpenAPI document at /v1/<realm>/_openapi.json to everyone, not only to its administrators. |
app_config.cache_ttl | 5m | How long clients may use the app configuration without asking again, from 1s to 1d. |
audit.retention | 365d | How long audit records are kept, at least 1d. |
admin.read_access.users | false | true: read-only administrators (admin: read) can also read users (emails, roles, sessions). Users hold personal data, so a realm opts in. |
admin.read_access.data | false | true: read-only administrators can read documents through the admin data route. |
admin.allowed_networks | none (any network) | IP addresses or CIDR networks, IPv4 or IPv6, allowed to use the admin API. See Network restrictions. |
default_locale | en | The language used for a user who has none and for email and push templates that lack the user’s. Must be in locales. See Languages |
locales | [default_locale] | The languages the realm supports: each user’s locale is one of them, and every listed language needs every email template and page. Shared by email and push notifications |
email | none (no email) | How the realm sends email: the delivery function, the sender, backd’s public address for links, the pages the links open, where users go afterwards, and limits. backd never sends mail itself. See Email |
email.customizable, email.overrides.permission | none, manage_org | The kinds of email an organization may write its own template of, and the capability that edits them. See company email templates |
pages.data_hook | none | An internal, mode: sync function (<database>/<function>) that fills in {{.App}} in the hosted pages with a company’s branding; email.data_hook does it for emails. See company context |
providers | none | The external identity providers users may sign in with (google, microsoft, apple), each with its client id and secret:NAME references. See Sign in with providers |
push | none (no push) | Push notifications: the platforms and their providers, the web subscription hosts and the device limits. Needs BACKD_SECRETS_KEY. See Push notifications |
sign_in.allowed_redirects | email.allowed_redirects | Origins and app schemes a provider sign-in may send the user back to |
sign_in.app_url | none | Where a sign-in goes back to when it has no redirect_to; within the allowed redirects |
mfa.enabled | true | false: users can’t turn a second factor on (users who have one are still asked for it). See Multi-factor authentication |
mfa.issuer | the realm’s name | The name authenticator apps show for the account |
magic_link.enabled | false | Let users sign in with a link or a code emailed to them (needs email: and BACKD_SECRETS_KEY). See Magic links |
magic_link.signup | false | An address with no account may make one (verified) by using a link or code, when the realm’s signup is open |
consents | none | The consents the realm asks for, each with required, current and per-version reaccept; texts live in consents/<name>/<version>/<locale>.md. See Consents |
compliance.consent_proof | [ip, user_agent] | What each consent record keeps besides time, method and text hash |
compliance.consent_retention | 3y | How long consent records are kept after their user was erased; 0 deletes them at erasure |
reauth.ttl | 5m | How long a re-authentication token works (1m to 1h) |
mfa.required_roles | none | Roles that must use a second factor, or all. A sign-in without one sets it up first. See Required by role |
account.require_verified_email | false | true: no session until the address is verified. See Account |
account.allow_email_change | false | true: users may change their own address |
account.welcome_email | false | true: send the welcome email once the address is verified |
account.purge_unverified_after | off | Delete accounts that never verified after this long (at least 1m; never accounts with roles) |
account.on_signup | none | An internal, async function (<database>/<function>) called for every new user, with {user_id, provider, profile?}, to create the app’s profile document. See The profile and the sign-up hook |
account.tokens.verify_email | 48h | How long the verification link works |
account.tokens.reset_password | 1h | How long a password reset link works |
account.tokens.change_email | 24h | How long the confirmation link of an email change works |
account.tokens.revert_email_change | 7d | How long the link to undo one works |
functions.max_concurrency | none (unlimited) | Caps how many of the realm’s functions may run at once on this backd instance; with several instances, up to that many times this value run at once. |
functions.log_retention | 7d | How long function invocation records are kept, at least 1h. |
functions.job_retention | 24h | How long an async job’s result is kept once done, at least 1h. |
Durations are a whole number of days (30d) or Go durations (12h, 90m), with a minimum of one minute.
signup, sessions, password, login_throttle, roles, admin, audit, email, providers, sign_in, account, functions.log_retention and functions.job_retention only apply when auth is enabled. Setting them in a realm with auth: disabled is an error, so a realm can’t carry settings that do nothing. cors and functions.max_concurrency apply either way.
CORS
Browsers only let a web page call backd from another origin if the realm allows that origin. List the origins of your web apps under cors.origins:
cors:
origins:
- https://app.example.com
- http://localhost:5173
- The list applies to every route of the realm: data,
/_authand/_admin. - Preflight requests (
OPTIONS) from a listed origin get204with the allowed methods (GET,POST,PUT,PATCH,DELETE) and request headers (Authorization,Content-Type,If-Match,If-None-Match,Idempotency-Key,Prefer,X-Backd-Reauth,X-Backd-Types,X-Request-ID). The answer may be cached for 10 minutes. - Every response under
/v1/carriesVary: Origin, so caches keep answers for different origins apart (see Caching). - Responses to listed origins, errors included, carry
Access-Control-Allow-Originand exposeETag,Idempotent-Replayed,Location,Preference-Applied,Retry-After,WWW-Authenticate,X-Backd-Types-StaleandX-Request-ID. - Credentials travel in the
Authorizationheader, so responses don’t allow credentials, except in a realm with session cookies (sessions.cookie): there, responses to the listed origins carryAccess-Control-Allow-Credentials: true, and a wildcard origin is refused. X-Backd-On-Behalf-Ofisn’t allowed from browsers: only server-side services with API keys may use it.*allows any origin. That’s reasonable for public, read-only data; for apps with users, list the origins explicitly.
CORS only tells browsers which pages may read responses. It isn’t access control: requests from other origins, and from anything that isn’t a browser, still go through authentication and access rules.
Account
account:
require_verified_email: false
welcome_email: false
allow_email_change: false
purge_unverified_after: 30d
tokens:
verify_email: 48h
reset_password: 1h
change_email: 24h
revert_email_change: 7d
Each of require_verified_email, welcome_email, allow_email_change and purge_unverified_after needs the email section: without one, startup fails naming the key. What they do is described in Email verification.
Roles
Each key under roles declares a role name. Role names follow the same rules as realm names: lowercase letters and digits, optionally separated by single - or _.
descriptionis optional and only for readers.adminmakes it an admin role: sessions of users holding it can use the admin API, as admin API keys can.admin: trueopens every area of it to change;admin: readis a read-only level for developers (every area to read, users and data only ifadmin.read_accessgrants them, nothing to change); a list opens only those areas:admin: [users, invitations], and may includeread:admin: [read, secrets]reads everything and changes secrets. The areas areusers,invitations,apikeys,secrets,audit,functions,dataandconfig; a user’s roles add up. See Admin rights for what each opens and the rules that stop an administrator from handing out more than they hold.backd bootstrapgives a realm’s first administrator a role withadmin: true. Several roles can be admin roles. At startup,backdwarns about a realm that declares no admin role, one with no role that hasadmin: true, or whose admin roles nobody holds (inrealm.yamlor the database).userslists the users who get the role: plain email addresses, or objects with anemailkey (- email: lead@example.com). These are seed assignments: they are applied bybackd provisionand every timebackd servestarts, and to a listed email as soon as that user signs up or is created. They add the role and never remove it.
Other assignments can be made later with backd user add-role or the admin API, without editing the file; those live only in the database. At startup, backd logs how many users have assignments that realm.yaml doesn’t list, and warns about users holding roles that realm.yaml no longer declares.
Access rules check roles with hasRole(user, 'admin') or 'admin' in user.roles. Changes to a user’s roles apply from their next request.
Emails are trimmed and lowercased, and each can appear only once per role.
Organizations
Organizations are groups of users that documents can belong to, such as your customers’ companies. The section is optional; without it the realm has none. Organization roles are separate from the realm roles above: they mean something only inside one organization, and what they allow is declared here.
organizations:
roles:
owner: { manage: [org, members, roles, teams] }
admin: { manage: [members, teams] }
editor: {}
member: {}
default_roles: [member]
create: user.email_verified
creator_roles: [owner]
max_created_per_user: 5
invitation_link: "https://app.acme.example/join?token={token}&org={org}"
| Key | What it says | Default |
|---|---|---|
enabled | false switches the section off without removing it | true |
roles | The organization roles: a name (same rules as realm roles), an optional description, manage (a capability or a list of them) and require_mfa (the role counts only for sessions that used a second factor) | required |
hooks | Membership hooks: on_member_joined, on_member_left and on_roles_changed, each an internal, async function <database>/<function> | none |
team_roles | Roles held within one team: a name, an optional description, grants (team roles its holders may give, in their own team) and require_mfa | none |
default_roles | Roles every member holds without being granted one. They can’t have manage capabilities | none |
create | An expression that decides who may create organizations; it sees user. "false" leaves it to realm administrators | user.email_verified |
creator_roles | The roles the creator of an organization holds. One of them needs manage: org | [owner] |
max_created_per_user | How many organizations a user may create, deleted ones included | 5 |
max_memberships_per_user | How many organizations a user may belong to (at most 100) | 100 |
max_teams_per_org, max_teams_per_member, max_pending_invitations | Caps for teams and invitations | 100, 20, 500 |
allow_require_mfa | Whether an organization may require a second factor of its members | true |
members_visible_to | Who sees other members’ emails: members, or managers (holders of any role with a manage capability) | members |
invitation_link | Where invitation links point: an absolute URL with {token} once (and {org} if you want it) within sign_in.allowed_redirects | none |
The capabilities of manage:
| Capability | Lets holders |
|---|---|
org | rename the organization and change its slug, and delete it |
members | remove members and change their roles, within what they hold themselves |
roles | give and take away any role, including roles with capabilities |
teams | create and change teams |
Startup checks the section and names what is wrong: unknown capabilities, no role with manage: org, a create rule that doesn’t compile, an invitation_link outside the allowed redirects. Organizations need auth: enabled. A role removed from this section stays in the stored memberships and grants nothing; backd logs it at startup. Which collections belong to organizations is declared in collection.yaml. Realm administrators with the users area manage organizations with backd orgs and the admin API; people use the organizations API.
Network restrictions
Three settings limit where people may act from. Client addresses come from TRUSTED_PROXIES, so every one of these depends on it being exact: behind a proxy without it, every request seems to come from the proxy.
admin:
allowed_networks: [10.20.0.0/16, "2001:db8:20::/48"]
roles:
ops:
admin: true
users:
- email: ada@example.com
admin_networks: [10.20.1.0/24] # her admin API requests
login_networks: [10.20.0.0/16] # her login and her session
- bob@example.com # no restrictions of his own
| Setting | Limits | Outside it |
|---|---|---|
admin.allowed_networks | Every admin API request, with an admin key or an admin session | 404 not_found, as if the admin API didn’t exist; credentials aren’t even checked. Logged as a WARN with the client address |
admin_networks (in a role’s user entry) | That user’s admin API requests. Must lie within admin.allowed_networks, or backd doesn’t start | 404 not_found, logged |
login_networks (in a role’s user entry) | That user’s login, and every request made with their session | Login answers 401 invalid_credentials, as for a wrong password, and counts as a failed attempt; requests with the session answer 401 unauthenticated. The session isn’t revoked: it works again from an allowed network |
- An empty or missing list means no restriction. At startup,
backdlogs (atinfo) each realm whose admin API is reachable from any network. - A user’s networks set in
realm.yamlare applied like role seeds, at startup and when the user is created, and they win over changes made elsewhere. Networks set for usersrealm.yamldoesn’t cover live only in the database; startup logs how many users have them. - An email listed in several roles must have the same networks everywhere.
- API keys can be limited too, when they’re created.
Realtime
The realtime: section lowers, for this realm, the instance’s limits on realtime streams. It can’t raise them.
realtime:
max_subscriptions_per_stream: 10 # default: the instance's (20)
max_streams_per_user: 2 # default: the instance's (5)
Triggers
The triggers: section sets the realm’s limits for collection triggers. A realm may lower the instance’s values and never raise them.
triggers:
max_per_collection: 20 # triggers one collection may declare (default 20)
max_chain: 16 # how deep a chain of trigger runs may go (default 16, at most BACKD_TRIGGER_MAX_CHAIN)
max_chained_per_minute: 60 # chained events one trigger may cause per minute (default 60)
max_backlog: 100000 # unfinished trigger jobs before events are recorded as gaps (default 100000)
Triggers need auth enabled in the realm. Operating them: lease, gaps, backd triggers status and rescan.
Storage
A realm that lets documents carry files connects its own S3-compatible bucket with a storage: section: the provider, bucket, a required prefix, and the access keys as secret:NAME references to realm secrets. See Connecting storage and the page of your provider (AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces). backd template realm writes it commented, with an example for each.
Validation
realm.yaml is read strictly. backd refuses to start, naming the file, when:
- the realm has no
realm.yaml; - the YAML is invalid or has an unknown key (a typo such as
idle_timoutis an error, not ignored); - a value is out of range or not one of the allowed values;
- a user setting is present while
authisdisabled; - a role name or email address is invalid, or an email is listed twice for a role;
- a network is invalid, a user’s
admin_networksisn’t withinadmin.allowed_networks, or an email has different networks in two roles.