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:
strategies:
password: [password]
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
Every realm needs an auth key: either auth: disabled (no authentication, for local work only) or a section with strategies, the ways people can sign in. Everything else has a default.
Keys
| Key | Default | Meaning |
|---|---|---|
auth | required | disabled (no authentication: never expose such a realm to the internet) or a section with the keys below. With a section, data routes need credentials (see who can access data) and provisioning creates the realm’s system database. |
auth.strategies | required | The ways to sign in: each strategy is a name and an ordered list of methods, for example web: [password] or admin: [password, totp]. Methods are password, magic_link, totp (only after another method) and the providers of auth.providers. See Authentication strategies |
auth.signup | closed | Who can create accounts: open (anyone), invite (needs an invitation), closed (only operators, with the CLI or the admin API). |
auth.sessions.idle_timeout | 30d | A session expires after this long without use. |
auth.sessions.max_lifetime | 90d | A session never lives longer than this. Must be at least idle_timeout. |
auth.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 |
auth.sessions.cookie.same_site | lax | lax, strict or none (an app on another site than the API) |
auth.sessions.admin_idle_timeout | idle_timeout | The idle timeout for users who hold an admin role. Can only shorten idle_timeout. |
auth.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. |
auth.password.min_length | 12 | Minimum password length, from 8 to 128. |
auth.login_throttle.account_threshold | 5 | Failed logins an email address gets before each new attempt must wait (1 or more). See Brute-force protection. |
auth.login_throttle.ip_threshold | 50 | The same per client address (an IPv6 /64), higher because a network can hide many people. |
auth.login_throttle.window | 15m | How long a failure counts: a counter expires this long after its last failure. At least 1m. |
auth.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 organization email templates |
pages.data_hook | none | An internal, mode: sync function (<database>/<function>) that fills in {{.App}} in the hosted pages with an organization’s branding; email.data_hook does it for emails. See organization context |
auth.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 |
auth.sign_in.allowed_redirects | email.allowed_redirects | Origins and app schemes a provider sign-in may send the user back to |
auth.sign_in.app_url | none | Where a sign-in goes back to when it has no redirect_to; within the allowed redirects |
auth.passkeys | none (off) | Settings for passkeys (enabled, rp_id, rp_name, origins). Signed-in users add passkeys to their account, and a strategy that starts with passkey signs people in with one (Passkeys). rp_id is the domain passkeys are bound to and cannot change later without losing every registered passkey; origins are https:// (http only for localhost with BACKD_DEV), within rp_id and within auth.sign_in. See the commented section of backd template realm. |
auth.totp.enabled | true | false: users can’t turn a second factor on (users who have one are still asked for it). See Multi-factor authentication |
auth.totp.issuer | the realm’s name | The name authenticator apps show for the account |
auth.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 |
auth.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 _realm/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 |
compliance.export | false | true: users can download a copy of their personal data and administrators can produce one. Needs storage: and email:. See Personal data export |
compliance.export_page | none | Your app’s download page, the one the “your data is ready” email links to (?export=<id> is added); required with export, within allowed_redirects |
compliance.export_retention | 7d | How long a package is kept before it is deleted |
auth.reauth.ttl | 5m | How long a re-authentication token works (1m to 1h) |
roles.<name>.strategies | all strategies | The strategies that keep the role active. A role that lists only strategies with a totp step needs a second factor from its holders: 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.
auth.strategies and the other keys of auth, roles, admin, audit, email, account, functions.log_retention and functions.job_retention only apply when the realm has an auth section. 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 (auth.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-server 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-server provisionand every timebackd-server 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, sso] }
admin: { manage: [members, teams] }
editor: {}
member: {}
default_roles: [member]
create: user.email_verified
creator_roles: [owner]
max_created_per_user: 5
sso: { providers: [google, microsoft] } # optional: organizations connect their own provider
invitation_link: "https://app.acme.example/join?token={token}&org={org}"
signin_link: "https://app.acme.example/{slug}/signin" # optional: the "which companies am I in?" email
| 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 |
sso | Organization sign-in: enabled (default true once the section is there), providers (google, microsoft; default both) and max_per_org (default and most: the number of providers). Without the section, or with enabled: false, the whole surface answers 404. Needs a role with manage: sso | off |
invitation_link | Where invitation links point: an absolute URL with {token} once (and {org} if you want it) within auth.sign_in.allowed_redirects | none |
signin_link | Where an organization’s sign-in page is, for the “which companies am I in?” email: an absolute URL with {slug} (the organization’s id when it has none; {org} is always the id) within auth.sign_in.allowed_redirects. Needs email:. Without it POST /_auth/orgs/find answers 404 | 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 |
sso | set up, replace and remove the organization’s sign-in connections, when the realm turns sso on |
Startup checks the section and names what is wrong: unknown capabilities, no role with manage: org (or manage: sso when sso is on), a create rule that doesn’t compile, an invitation_link without {token} or {org}, or outside the allowed redirects. Organizations need an auth section. 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.