Operate
Running backd: MongoDB, deployment, providers, administration, monitoring, backup and hardening.
This section is for the operator: whoever deploys, secures, monitors, backs up and upgrades a backd service, and does not necessarily write the application on it. Building an application is Build; changing backd itself is Contribute; a message you do not recognize may be in Troubleshooting.
Where to find things
| Task | Page |
|---|---|
| Deploy for real: TLS, MongoDB, rate limits | Production deployment |
| Run it on Docker Compose or Kubernetes | Docker Compose, Kubernetes |
| Build it on a cloud provider | Cloud providers, starting with DigitalOcean |
| Run the functions executor, egress and workers | Executor, workers and egress |
| Package and promote the config | Deploying config |
| Check before exposing it | Hardening checklist and the security model |
| Run it: users, API keys, secrets, the audit trail | Administration and the command line |
| Watch it | Metrics and the change feed |
| Back it up and restore it | Backup and restore and Keeping storage tidy |
| Connect email, storage and push providers | Providers |
| Something leaked | If credentials leak |
| Let companies sign in with their own Google or Microsoft account | Organization sign-in |
| Move to a new version | Upgrading to below |
| Find a word I don’t know | Glossary |
How the pieces fit
A deployment is a few processes around MongoDB. The reverse proxy and the storage are yours; the rest are backd-server in different roles, plus the executor’s Deno processes.
flowchart LR
client["browsers, apps, services"] --> proxy["reverse proxy
TLS, rate limits
(yours, not part of backd)"]
proxy --> api["backd-server serve
the API; add --with-worker
to run jobs too"]
api --> mongo[("MongoDB
a replica set")]
api --> storage[("S3-compatible storage
files")]
api --> exec["executor
one Deno process per call"]
exec -. "ctx.db, over an
internal listener" .-> api
worker["backd-server worker
async jobs, cron"] --> mongo
worker --> exec
exec --> egress["egress
only declared hosts"] --> net["the internet"]- API (
backd-server serve): authenticates callers, applies rules, validates documents, answers. Run as many as you like with the same config. - Worker: claims async jobs and creates the runs of cron schedules. Without one, jobs stay
queued. - Executor and egress: run the functions, and give them their only way out. See Executor, workers and egress.
- MongoDB holds all the state; the processes keep none, which is why you can run several side by side.
MongoDB
backd needs MongoDB 8.2 running as a replica set. A single node is fine. Replica sets support transactions, which atomic batch writes rely on, and every setup backd is tested with uses one. Sharded clusters (through mongos) work too.
At startup, backd asks the server what it is. It refuses to start against a standalone server:
mongodb: the server is a standalone instance; backd needs a replica set (a single node is fine): start mongod with --replSet and run rs.initiate(), then add ?replicaSet=<name> to MONGO_URI
Managed services such as MongoDB Atlas always run replica sets. To run a single node yourself:
docker run -d --name mongo -p 127.0.0.1:27017:27017 docker.io/library/mongo:8.2 --replSet rs0
docker exec mongo mongosh --quiet --eval \
"rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'localhost:27017'}]})"
export MONGO_URI='mongodb://localhost:27017/?replicaSet=rs0'
- The member’s
hostis the address clients use to reach it:localhost:27017for a server on your machine, the service name (such asmongo:27017) inside a compose network. replicaSet=rs0inMONGO_URInames the set. Without it, the driver still discovers the set from the host you give.- With authentication, a replica set also needs a key file (
--keyFile), even with one node. The production reference shows how. make dev,make exampleand the test setups start a single-node replica set and initiate it from the container’s health check.
Upgrading an existing standalone server
Restart it as a replica set and initiate it once; the data stays in place:
- Stop
backd. - Restart
mongodwith--replSet rs0(and--keyFileif authentication is on). - Run
rs.initiate({_id: 'rs0', members: [{_id: 0, host: '<host>:27017'}]})once, as the admin user if authentication is on. - Add
replicaSet=rs0toMONGO_URIand startbackd.
Provisioning
At startup, backd makes sure MongoDB matches the config. What it does depends on PROVISION_MODE.
apply (default)
- Creates each missing collection with its
$jsonSchemavalidator. MongoDB creates the database implicitly. - Updates (
collMod) validators that differ fromschema.json. - Creates indexes declared in
indexes.jsonthat don’t exist yet. - Creates or updates the system database of every realm with an
authsection. - Enables pre-change images (
changeStreamPreAndPostImages) on collections that declarerealtime: true. They are never switched off by a start: see Disabling them. - Records the config’s fingerprint for each realm (see Deploying config).
- Is idempotent and safe to run on every start.
- If a declared index exists with a different
uniquesetting,applystops with an error rather than dropping it. Drop the index manually and restart. - Creating a unique index fails if the collection already holds duplicate values. The error names the index.
verify
Only compares collections, validators, indexes and pre-change images with the config, including the system databases.
If anything differs, it refuses to start and lists every difference. For example:
backd-server serve: MongoDB does not match the config (run `backd-server provision`): shop__orders.items: collection missing shop__orders.clients: validator differs from schema.json shop__orders.clients: index (email) missing shop__orders.live: pre-change images are not enabled (this collection is realtime)Then compares the config’s fingerprint with the one provisioning recorded for each realm, and refuses to start if it differs or none was recorded: every instance must run the config that was provisioned (see Deploying config).
Needs only read/write privileges on the data, and read access to
backd___deployment.realms.
Common to both modes
- Validators always use
validationLevel: "moderate"andvalidationAction: "error". - Nothing is ever dropped. Some databases, collections and indexes exist in MongoDB but not in the config: databases named like
<realm>__<database>, collections inside configured databases, or indexes not inindexes.json.backddoesn’t serve those databases and collections, keeps those indexes, and logs a warning for each. The same goes for a<realm>___systemdatabase whose realm is gone or hasauth: disabled: it is kept and reported, never used. - Schema keywords that MongoDB can’t enforce are logged at
infolevel (see Schema validation).
backd-server provision runs apply once and exits.
Both backd-server provision and backd-server serve then apply the role assignments seeded in each realm’s realm.yaml.
Realm system databases
Each realm with an auth section has a system database named <realm>___system (three underscores). It holds the realm’s users and credentials, managed through the admin API (for example with backd user); only backd-server bootstrap writes to it directly. Realm names must be short enough for this name to stay under MongoDB’s 64-character limit, so a realm name can be at most 54 characters.
| Collection | Holds | Indexes |
|---|---|---|
users | email, verified flag, roles, disabled flag | email (unique, sparse) |
identities | sign-in methods per user: a password hash, or an external provider’s subject and address (and, for Apple, a sealed refresh token) | provider + subject (unique), user_id + provider (unique), user_id |
sessions | sessions: public id, hashed token, last use and expiry | token_hash (unique), user_id, expires_at (TTL) |
api_keys | hashed API keys with name, first characters, last use and expiry | name (unique) |
invitations | invitations: public id, hashed token, optional email | token_hash (unique), expires_at (TTL) |
login_attempts | failed-login counters per email and client address; reused, under their own key namespace, for function rate limits | expires_at (TTL) |
audit | the audit trail: append-only records of security-sensitive actions | at, target + at, expires_at (TTL) |
secrets | function secrets, encrypted at rest (never their plaintext values) | database + name (unique) |
invocations | function invocation history: append-only, what happened — never a call’s input or output | function + at, request_id (sparse), expires_at (TTL) |
jobs | async function jobs: input, status, and once done, result — the one exception to never storing input/output, since there’s nowhere else to keep it until read | status + lease_expires + created_at, expires_at (TTL) |
oauth_states, login_codes | the short-lived records of the provider sign-in: an attempt waiting for the provider, and a one-time login code (it records which sign-in method made it) waiting to be redeemed; both hashed, used once | expires_at (TTL) |
auth_challenges | the steps between a first factor and a session: a second factor to give; hashed, used once, 5 minutes | expires_at (TTL), user_id |
devices | the push devices users registered: the token sealed with the realm’s key, looked up by its hash | token_hash (unique), user_id, session_id, last_seen |
mfa | one document per user with a second factor: the TOTP secret (encrypted) and the recovery codes (keyed hashes) | _id is the user |
passkeys | one document per registered passkey: the authenticator’s credential id, its public key and flags, the relying party id it was registered under (public data, no secret) | _id is backd’s id; credential_id is unique |
idempotency | Idempotency-Key claims: scoped to the function and caller, a sync call’s stored response or an async call’s job id | expires_at (TTL) |
Each collection has a $jsonSchema validator that checks its required fields and their types. The TTL indexes make MongoDB delete expired sessions, invitations, counters, audit records, invocation records, jobs and idempotency claims automatically. If one of these indexes exists with different options (for example expires_at without TTL, so sessions would never expire), apply and verify fail and name it; drop the index and let backd recreate it.
Treat the system databases as sensitive: they contain password hashes and token hashes. Back them up with the rest of the realm’s data, encrypted: only role seeds can be rebuilt from the config. Tokens and keys are stored only as hashes, so a leaked backup doesn’t reveal usable credentials, but password hashes can still be attacked offline. See Backup and restore, and If credentials leak for what to do if one does.
Least-privilege deployments
The service’s MongoDB user doesn’t need to create collections, change validators or build indexes. Run provisioning as a separate deploy step instead:
- Run
backd-server provisionwith an admin-privilegedMONGO_URI. - Grant the service’s user
find,insert,update,removeandlistIndexeson each collection thatbackd databases --collectionsprints, andlistCollectionson their databases. MongoDB privileges can’t match names by pattern, so repeat this step whenever the config changes. - Start the service with
PROVISION_MODE=verifyand that user.
Grant collections rather than MongoDB’s built-in readWrite role on whole databases: readWrite also allows creating, dropping and re-indexing collections, and MongoDB lets insert on a database create any collection in it. The production reference automates all three steps.
backd databases needs only CONFIG_DIR. It prints every data database (<realm>__<database>) and the system database (<realm>___system) of every realm with an auth section. With --collections, it prints every collection as <database>.<collection>, including the system collections:
$ CONFIG_DIR=./config backd databases
blog___system
blog__main
shop__orders
$ CONFIG_DIR=./config backd databases --collections
blog___system.api_keys
…
blog__main.posts
shop__orders.items
Upgrading to the strategy release
v0.9.10 changes how sign-in is configured. Every realm.yaml needs editing before the new binaries start; nothing changes in the data or the indexes, and the HTTP API only grows (new fields and one route). A realm that still has the old keys stops at startup, with a message that names where each key went; backd config check prints them all at once.
| Before | Now |
|---|---|
auth: enabled | auth: with strategies: (auth: disabled stays as it was) |
signup, sessions, password, login_throttle | auth.signup, auth.sessions, auth.password, auth.login_throttle |
providers, sign_in | auth.providers, auth.sign_in |
magic_link, reauth | auth.magic_link, auth.reauth |
mfa (enabled, issuer) | auth.totp |
mfa.required_roles: [a, b] | a strategy with a totp step (a_totp: [password, totp]) and strategies: [a_totp] on the roles a and b |
mfa.required_roles: all | every strategy ends with totp |
The smallest conversion of a realm that had auth: enabled and signup: open is:
auth:
strategies:
password: [password]
signup: open
What else changes, and needs your action:
- A user’s own authenticator adds no step any more. Before, a regular user who turned TOTP on was asked for a code at every sign-in. Now only a strategy that lists
totpasks for it. A realm that wants the code from everyone writespassword: [password, totp]. - Several strategies that start with the password need the clients to name one. With one such strategy nothing changes for the apps. With two (the template’s
passwordandadmin_password), a sign-in that names none answers400 strategy_required: passstrategy(backd login --strategy, the clients’ option) or set it once in the client. - Roles can list strategies. A role that lists some is active only in sessions made with one of them; a role that does not is active in every session. Sessions made before the upgrade keep every role until they expire.
- A sign-in through an organization’s own connection is now the method
ssoof a strategy. A realm withorganizations.ssoneeds a strategy that starts with it (sso: [sso]), or its connections sign nobody in (400 unknown_strategy). Before, such sign-ins asked for no second step and kept every role. - Clients.
mfa_requiredandmethodsstay in the challenge answer next to the newnextandstrategy. The Dart and Python clients, the JavaScript client and the Go client take astrategy; older clients keep working against a realm with one password strategy.
How strategies work is in Authentication strategies. v0.9.10 also adds passkeys, off until a realm sets auth.passkeys.enabled: they add a passkeys collection to the provisioned indexes (run backd-server provision as usual) and a passkey_handle field to users, and change nothing for a realm that does not use them.
Upgrading to v0.9.9
v0.9.9 changes nothing in the data, the provisioned indexes, the configuration or the command line: replace the binaries or the images and run backd-server provision as usual. It is built with Go 1.27.4, which fixes GO-2026-6859 in net/http, so it is the version to run if you were on v0.9.8. The clients move to 0.9.9 with the server (for Dart, backd: ^0.9.9).
Upgrading to v0.9.8
v0.9.8 changes nothing in the data, the provisioned indexes or the configuration: run backd-server provision as usual (it was backd provision until v0.9.7). What changes is the command line: backd becomes two binaries, released together with one version: backd-server runs the server and the tools that work on MongoDB or run Deno, and backd is the command line for developers and administrators. Nothing changes in the data or the configuration, but the commands below moved, with no alias:
| Was | Now |
|---|---|
backd serve, worker, executor, egress, provision | backd-server serve, worker, executor, egress, provision |
backd functions build | backd-server functions build (in CI, in the backd-executor image) |
backd bootstrap | backd-server bootstrap |
backd secret rotate-key | backd-server secret rotate-key |
backd secret create|rotate|list --direct | backd-server secret create|rotate|list |
backd user mfa-reset --local | backd-server user mfa-reset |
backd export --direct, backd import --direct | backd-server export, backd-server import |
backd triggers rescan | backd-server triggers rescan |
- Images. Both images carry both binaries. The
backdimage’s entrypoint is nowbackd-server(its command is stillserve), and thebackd-executorimage’s isbackd-server executor. A compose file or chart that passes only a role as the command keeps working; one that setsentrypoint: ["/usr/local/bin/backd"]for a server role sets/usr/local/bin/backd-server.docker compose exec backd /backd bootstrapbecomesdocker compose exec backd backd-server bootstrap. - Release archives.
backdships for Linux, macOS and Windows, andbackd-serverfor Linux; install the CLI on the machines you administer from. - Dev mode builds the functions at startup and waits for a config that loads instead of exiting (Dev mode): the local stacks’ one-shot
functions-buildservice is gone. If you copied one of those stacks, delete the service and thedepends_onon it; scripts that waited for/healthzwait for/readyznow, because/healthzanswers before a config has loaded. - Version check. The CLI sends
User-Agent: backd/<version>and warns once when an authenticated admin answer says the server’s version differs. A CLI built from source (backd/dev) is refused with403 dev_client_refusedby a released server outside dev mode. See the CLI. - Rules that follow a reference are new, and optional. A child collection can use
parent(document.list_id).membersin its rules (Rules that follow a reference); nothing existing changes meaning. If you use it, index the parent’s field the rule reads and the child’s reference field, and give both collectionsrealtime: trueif the child streams. - Running on Kubernetes and DigitalOcean is documented and shipped (a Helm chart and Terraform): see Kubernetes and Deploying on DigitalOcean. Compose deployments need nothing.
- Move the clients with the server.
backd-js,package:backd, the Pythonbackdand the Go client are released on the same version:0.9.8(for Dart,backd: ^0.9.8).
Upgrading to v0.9.7
v0.9.7 changes nothing in the data, the provisioned indexes or the configuration: run backd provision as usual. It adds BACKD_MONGO_DEBUG, a diagnostics switch that is off by default. The clients move to 0.9.7 with the server (for Dart, backd: ^0.9.7).
Upgrading to v0.9.6
v0.9.6 changes nothing in the data or the provisioned indexes: run backd provision as usual. What changes:
- Sizes follow one grammar. A number and a case-sensitive unit with no space (
512KiB,1.5GB); decimal units are powers of 1000, binary ones of 1024. A function’smemorytakes binary units only:256MBis refused, write256MiB. Runbackd config checkon your configuration before deploying. See Sizes and units. backdandbackd executormust run the same version. The request to the executor carriesmemory_bytesinstead ofmemory_mb: upgrade both together, or an older executor refuses the limit.- Environment variables with a size (
MAX_BODY_BYTES,BACKD_MAX_UPLOAD_BYTES,BACKD_REALTIME_MEMORY) accept a unit as well as a number of bytes; existing values keep working. - New clients.
backd types --lang goand--lang pythongenerate types for the Go client (pkg/client) and the Python client (backdon PyPI). Existing languages are unchanged. - Move the clients with the server.
backd-js,package:backdand the Pythonbackdare released on the same version:0.9.6(for Dart,backd: ^0.9.6).
Upgrading to v0.9.5
v0.9.5 adds public files. Nothing changes for a realm that doesn’t use them: the configuration, the data and the provisioned indexes stay as they are, and backd provision has nothing new to do. What changes:
- A file field can be
public: true, with a second, independentpublic_storage:inrealm.yaml(its own provider, bucket, keys and theurlbrowsers read from). A realm withoutpublic_storage:that declares a public field fails the configuration check. Read the security notes first: anyone with a public file’s address can read it, and the bucket’s public readability is yours to set up. - Regenerate the types (
backd types, andbackd types --functions): every file in a realm with files gainspublicandkey_name, and a version gainsurl. Committedtypes.d.ts,types.dartandbackd.d.tsfiles show a diff; a CI check with--checkfails until they are regenerated. backd storage checkandreconciletake--storage private|public(private by default, as before), and--directtakes--public-storage-keyand--public-storage-secret. The same choice exists as?storage=public/ thestoragebody field in the admin API.- Exports and imports carry public files. An import checks each one like an upload (type, extension, name, metadata) before publishing it, and refuses a picture that stripping would change.
FileLink.expiresAtis nullable inpackage:backd(a public file’s link never expires): handlenullwhen you read it.- Move the clients with the server.
backd-jsandpackage:backdare released on the same version:0.9.5(for Dart,backd: ^0.9.5).
Upgrading to v0.9.4
v0.9.4 changes nothing in the configuration, the data or the provisioned indexes: run backd provision as usual, there is nothing new for it. What changes:
- The admin UI has a new look. Same pages and the same rights, with a top bar of grouped menus and a breadcrumb; the sign-in is one form with the realm as a field (links to
/_ui/r/<realm>/…still open the same realm). See the admin UI. - A user whose password was expired for them alone can choose a new one signed out with
POST /_auth/password/update(and the admin UI’s sign-in form offers it): the current password, and a code if the account has a second factor. A password expired for the whole realm (expire-passwords) cannot: it stays reset-by-email only, on purpose. See Change the password signed out. password_expired_atgoes back tonullwhen the user gets a new password. A script that reads it to find who is still expired now finds only those who are.- The images carry Deno 2.9.8. Running
backdoutside the images, install that Deno: a binary that finds another one says so. - Move the clients with the server.
backd-jsandpackage:backdare released on the same version:0.9.4(for Dart,backd: ^0.9.4).
Upgrading to v0.9.3
v0.9.3 adds organization sign-in (a company’s own Google Workspace or Microsoft tenant) and scopes every session to one organization in a realm that has organizations (see scoped sessions). A realm without organizations changes nothing. Do these in order:
- Give every organization a slug.
backd orgs listshows-for the ones without;backd orgs set-slug --id <org> --slug acme-corpgives one (startup also warns about them). People sign in by the slug; a slug may not look like an id. - Run
backd provisionwith an admin-privilegedMONGO_URI. Sessions gain an index onorgand the fieldsmax_lifetime, jobs gaincaller_organd API keysorg; theorg_connectionscollection is created, and the two unique indexes ofidentitiesbecome(provider, org, subject)and(user_id, provider, org)(nothing is rewritten, the old ones are dropped). WithPROVISION_MODE=verifythe instances refuse to start until it has run; with least-privilege grants,backd databases --collectionsprints the new collection. - Move the apps to the organization sign-in. Sign in at
/_auth/orgs/{org}/…(createClient({ org })in the clients), drop?org=,inOrg(),GET /_orgsand the/_orgs/invitations/*calls, and make the sign-up page ask for the team’s slug. The sign-in experience describes what to build. Realm administrators keepPOST /_auth/login; the admin UI andbackd loginneed nothing. - Expect everyone to sign in once more. Sessions opened before have no organization and answer
401 session_unscoped; the clients take that as an expired session. A session of a user who is an administrator keeps working. - Bind the keys that act on behalf of users.
backd apikey create --org <org>makes a key that counts one organization; the ones that exist count none until replaced. - Organization sign-in is off until you turn it on (
organizations.ssoinrealm.yaml, a role withmanage: sso,BACKD_SECRETS_KEYand a public address). See Organization sign-in for operators. - The images keep
backdat/usr/local/bin/backd(the plainbackdimage used to have it at/backd), with/backda link to it, so anentrypoint:orexecthat names either keeps working. - Move the clients with the server.
backd-jsandpackage:backdare released on the same version:0.9.3(for Dart,backd: ^0.9.3).
Upgrading to v0.9.2
v0.9.2 changes where a realm keeps its content (breaking, step 1) and adds the personal data export and live app configuration. Run backd config check against your tree with the new binary first: it lists every folder to move.
- Move the realm’s content folders into
_realm/.email,pages,push,consents,complianceandapp-configleave the top of each realm for<realm>/_realm/(app-configbecomes_realm/config);realm.yamland the databases stay where they are. For a realm calledacme:mkdir acme/_realm && mv acme/email acme/pages acme/push acme/consents acme/compliance acme/_realm/(only the ones you have), andmv acme/app-config acme/_realm/config. The old layout stops loading, and the error names each folder and where it goes;backd config checkshows it before you deploy. The config fingerprint changes, so every instance must run the new tree. - Run
backd provisionwith an admin-privilegedMONGO_URI. It creates theuser_exportssystem collection (the records of personal data exports). WithPROVISION_MODE=verifythe instances refuse to start until it has run. If you run with least-privilege grants,backd databases --collectionsprints the new collection. - Nothing else changes until you turn exports on (
compliance.export: true, which needsstorage:andemail:).data-export-readyanddata-export-madeare built-in email kinds;backd template realmwrites the English text and a_realm/compliance/readme/en.htmlto edit. - Move the clients with the server.
backd-jsandpackage:backdare released on the same version:0.9.2(for Dart,backd: ^0.9.2).
Upgrading to v0.9.1
v0.9.1 adds field encryption, breach-response tools (password expiry, ending every session, one-time reset links) and export and import with anonymized copies for staging. Nothing breaks for existing configuration, but the release adds system collections and validators, so it needs backd provision. What to check:
- Run
backd provisionwith an admin-privilegedMONGO_URI. It creates thedata_keysandsecurity_statesystem collections and updates the validators of the existing ones. WithPROVISION_MODE=verifythe instances refuse to start until it has run. - Field encryption and password expiry are off until you use them. A field is encrypted when
collection.yamlgives itfields.<path>.encrypt.key; create the key first withbackd secret create(it is shown once: save it). - Grant the service’s user the new collections if you run with least-privilege grants:
backd databases --collectionsprints them with the others. - Run
backd typesagain to getredactedin the generated TypeScript types;backd types --lang dartdoes the same for Dart. - The Dart client moves to the release train.
package:backd0.9.1 goes withbackdv0.9.1 andbackd-js0.9.1; it was0.0.5, and a caret on a0.0.xversion never picks a newer one, so changebackd: ^0.0.5tobackd: ^0.9.1. - Export and import are stricter. An import checks the paths and checksums of every part of a package and holds
_metaand file sizes to the fields’ rules; a package that was edited by hand may now be refused.
Upgrading to v0.9.0
v0.9.0 adds the Dart client and the generators around it. The server changes only add: no new collections, no new configuration, nothing to provision. What to check:
- Nothing breaks, and nothing needs provisioning. If you run
PROVISION_MODE=verify, nobackd provisionrun is needed for this release alone. backd types --lang dartwrites a realm’stypes.dart(aShelfRealmaccessor and document, input,where/orderBy, function and configuration classes) next to the TypeScript output, and the admin types endpoint takes?lang=dart.backd openapikeeps feeding openapi-generator for Kotlin, Swift and the rest;make openapi-clientsbuilds all three from the example realms’ documents.- The Dart client is
package:backdon pub.dev (dart pub add backd), versioned on its own: sign-in with the second-factor, magic-link, provider and consent steps, collections, files with direct uploads and image versions, function jobs, live lists over realtime, push devices, app configuration, organizations and generated realm types (BackdClient<ShelfRealm>). It has no admin API, by design: administration stays in the JavaScript client, the admin UI and the CLI.
Upgrading to v0.8.5
v0.8.5 adds realtime subscriptions and triggers, both built on MongoDB’s change stream. Nothing is on until a collection declares realtime: or triggers:, and nothing breaks for config that doesn’t. What to check:
- Run
backd provisionwhen you declarerealtime:ortriggers:, before you start instances withPROVISION_MODE=verify. Provision enables pre-change images on those collections (without them a removal can’t be told to the right subscribers, andverifyrefuses to start, naming the collection) and creates the newjobsindexes the trigger dispatcher uses (the per-realm backlog count, andserial_key/serial_orderfor ordered triggers). Droppingrealtime:from a collection leaves its images in place;backd provision --disable-preimagesremoves them. - Realtime needs MongoDB 6.0 or later as a replica set, which the change feed requires at startup. The stream is opened on the whole deployment, so the
MONGO_URIuser needs thechangeStreamandfindactions on every database — for example the built-inreadAnyDatabaserole; a confined user that can’t get it can’t serve realtime. See the change feed requirements. - Split the work per instance.
BACKD_EVENTS=falsekeeps realtime streams off request-serving replicas;BACKD_TRIGGERS=falsekeeps an instance from holding the trigger dispatcher’s lease. Both default totrue, and nothing is read until a collection declares the feature. - A trigger’s writes are stamped
_meta.chain({root, depth}) so chains can be bounded (triggers.max_chain,max_chained_per_minute,max_backloginrealm.yaml). A client can’t set it and a function can’t drop it.backd triggers status|gaps|resolveandbackd triggers rescaninspect and repair the dispatcher. - The JavaScript client is 0.8.5 (
npm install backd-js@latest):client.realtime.subscribe()andcollection.subscribe()live lists, the generated trigger input types and four typed errors (RealtimeNotEnabledError,TooManySubscriptionsError,InvalidResumeTokenError,SessionEndedError). It keeps working with a v0.8.4 server for what existed there; live lists need a v0.8.5 server.
Upgrading to v0.8.4
v0.8.4 adds reference expansion, aggregations and full-text search. Nothing is on until a collection declares it, and nothing breaks for config that doesn’t. What to check:
- Run
backd provisionwhen you declare a text index or the indexes an aggregation’s warning asks for, before you start instances withPROVISION_MODE=verify. A text index is created (and rebuilt when its declaration changes, with searches answering503 index_buildingmeanwhile); a collection with alanguage_fieldalso gets a server-owned_search_langin its validator and, once, in the documents that already exist. - Two names are now reserved in
schema.json:_expandedand_search_lang. A schema that declares a property with either name stopsbackdat startup, with the file named: rename the property. - New, opt-in features:
references:incollection.yamland?expand=on reads (BACKD_EXPAND_MAX_DEPTH,_PATHS,_ARRAY,_DOCS),aggregate:andGET …/_aggregate(BACKD_AGGREGATE_TIME,BACKD_AGGREGATE_MAX_GROUPS), a text index and$search.backdlogs a warning at startup for eachgroup_byfield that no index starts with. _batchstill only writes, so there is nothing to expand there;ctx.dbandctx.admin.dbin functions do expand, aggregate and search.- The JavaScript client is 0.8.4 (
npm install backd-js@latest):expand(),aggregate(),search(),fillBuckets()and typed_expanded. It keeps working with a v0.8.3 server for what existed there; the new calls need a v0.8.4 server. Types generated by an olderbackd typeskeep working; regenerate to get the new ones.
Upgrading to v0.8.3
v0.8.3 adds organizations and team access. It has no breaking changes: everything is off until realm.yaml declares organizations. What to check:
- Run
backd provisionbefore you start instances withPROVISION_MODE=verify. The release adds the system collectionsorgs,org_members,org_teams,org_invitations,org_member_seqandorg_templates, and indexes on collections that declareorgorteam.applycreates them at startup;verifyrefuses to start until a provisioning run did, and the least-privilege user of the production reference needs them granted. - Organizations are opt-in. Declare
organizationsinrealm.yaml(roles,team_roles,hooks,require_mfa) andorganization: required|optionalandteam: required|optionalincollection.yaml._meta.organd_meta.teamare server-owned. See organizations and the rules cookbook. - Rules, functions and the client gain surface:
inOrg,hasOrgRole,inTeamandhasTeamRolein rules,ctx.user.orgsandctx.admin.orgsin functions, and organizations in the JavaScript client and its admin API. Existing rules keep working. - The JavaScript client is 0.8.3 (
npm install backd-js@latest). It keeps working with a v0.8.2 server for what existed there; the organizations methods need a v0.8.3 server.
Upgrading to v0.8.2
v0.8.2 is a large minor release, and it has breaking changes, as allowed before a stable release: the first five points need your action, the rest are worth a look. Features are off until your config uses them. What to check:
- Run
backd provisionbefore you start instances withPROVISION_MODE=verify. The release adds system collections (oauth_states,login_codes,auth_challenges,mfa,devices,consent_records,app_config,app_config_history,push_sends) and one index on every data collection,_meta.created_at,id, which serves the new default order of lists.applycreates them at startup;verifyrefuses to start until a provisioning run did, and the least-privilege user of the production reference needs the new collections granted. The index is built once per collection: on a very large collection do it at a quiet time. - Lists without
order_byare ordered by_meta.created_at, thenid(before: byid). For generated ids that is almost the same; anaftercursor is now an opaque value, and the bare id works only withorder_by=id. Callers that builtafter=<id>by hand must addorder_by=id. See Querying. - A create that sends
idto a collection that generates ids is400 id_not_allowed(before, the field was ignored). Collections that declarecustom_idrequire it. - Sensitive account actions ask for a fresh proof of identity, and their
passwordfields are gone. Changing the password or the address, deactivating the account, unlinking a sign-in method, turning the second factor on or off, and creating an API key as an administrator signed in with a session answer401 reauth_requiredwithout anX-Backd-Reauthtoken fromPOST /_auth/reauth. The JavaScript client and the admin UI do it for you; direct callers of those endpoints must change. See Fresh proof. email.default_localeandemail.localesmoved to the top level ofrealm.yaml(default_locale,locales), shared by email and push. The old keys are refused at startup, with a message that says where they went. The sign-in return parameters of provider flows are now prefixedbackd_(backd_code,backd_error,backd_linked): a page that handles the return by hand must read the new names (backd-jsdoes it for you).- New, opt-in features: sign-in with Google, Microsoft or Apple, a second factor (
mfa:inrealm.yaml; the template’s admin role requires one, so administrators enrol on their next sign-in), magic links and accounts without a password, push notifications, consents and a personal data scope, app configuration, conditional GET and public caching, custom ids, generated TypeScript types and a per-realm OpenAPI document. Several of them seal values with the key ofBACKD_SECRETS_KEY: set it before you turn them on. BACKD_DEV=truereloads the whole config live (schemas, rules, indexes, functions and templates), and rewritesbackd.d.tsin each functions folder. Production behavior is unchanged.- The JavaScript client is 0.8.2, on the same version as the server from now on (
npm install backd-js@latest). It adds the second step and magic links, provider sign-in, push, consents,client.config, the cache, typed clients (createClient<Realm>()) and the custom-id errors. It keeps working with a v0.8.1 server for what existed there.
Upgrading to v0.8.1
v0.8.1 adds image versions, storage quotas and direct uploads in parts, and it has breaking changes: the first two points need your action, the rest are worth a look. Nothing else that worked on v0.8.0 changes. What to check:
- Move
rules.yamlintocollection.yaml. The rules are now itsrules:section (same keys). A collection directory that still has arules.yamldoes not load, so a forgotten file can’t leave a collection without rules: indent the file’s content two spaces underrules:incollection.yamland delete it.rules.test.yamlstays where it is. See rules. backdwithout a command no longer serves. It prints its help and exits 0. The images already runserve; a Dockerfile, compose file or unit that startedbackdalone must saybackd serve.- JPEG and PNG originals lose their metadata by default (EXIF with the GPS position, XMP, comments, PNG text), pixels untouched and orientation kept. Set
keep_metadata: trueon a field to keep it. A picture too damaged to be copied is refused with422 invalid_image. Files stored before the upgrade are not changed. See Metadata and the GPS risk. - Image versions and quotas are opt-in:
versions:under a file field (workers make the copies, so run them;?version=serves them andbackd files regenerateremakes them after a declaration changes) andstorage.quotainrealm.yaml. - Direct uploads above 5 GiB need a provider that checks a SHA-256 per part: AWS S3, Cloudflare R2 and MinIO. With DigitalOcean Spaces
max_sizeof a direct field stays at 5 GiB. A file sent in parts has asha256that is the digest of its parts’ digests, not of its bytes (why). Workers abort the uploads left unfinished. - Sizes accept
TBandTiB.GET …/_files/{field}addsmultipart_abovefor the fields that can hold more; the start of a direct upload takespart_sha256instead ofsha256for such a file. - API additions:
POST /_admin/config/check(a change checked against the realm’s whole configuration, without applying it) and the image version routes. The admin UI’s schema and rules editors use the first. - The JavaScript client is 0.8.0 (
npm install backd-js@latest):uploadFilesends large files in parts by itself, plus the version helpers andadmin.checkConfig(). It works with a v0.8.0 server for everything that existed before.
Upgrading to v0.8.0
v0.8.0 adds files, schema checks, on_complete and job steps, and more control over schedules (pausing, timezone, overlap: skip). Configuration that worked on v0.7.0 still loads, except for the one case in point 2; each new feature is off until you use it. What to check:
- Provision before rolling out, as always. New system collections are created in each realm’s
___systemdatabase:file_journal,file_deletions,storage_usage,schema_checksandschedules.PROVISION_MODE=verifyrefuses to start untilbackd provisionhas made them. - A file field can’t be declared in
schema.json. Files are declared underfiles:incollection.yaml(backd adds their schema), and startup refuses a collection that has the same property in both, and one withfiles:in a realm withoutstorage:. If you used a property to hold file details, move it. The tutorial’sassets.fileplaceholder moved this way. - Files are opt-in, and need more than configuration. A realm declares
storage:inrealm.yamland sets its two keys as realm secrets (backd secret set, withBACKD_SECRETS_KEYon the instance); without them the realm’s file endpoints answer503 storage_unavailableand reads of its documents come without links. Run workers (backd worker, or--with-worker): they delete the objects of replaced, removed and abandoned files and finish uploads a crash interrupted. SetBACKD_URLin production, since backd-signed links name it.BACKD_MAX_UPLOAD_BYTES(default 100 MiB) caps proxy uploads. - Give each backd instance its own storage
prefix, and read Security of files: links and caches outlive later rule changes, and a function can’t declare the storage secrets orBACKD_FILES_LINK_KEYas its own (startup refuses it). - Erasing a user now also queues the files of the documents the policy deletes or anonymizes (a
removeof a file field). ADELETE, a batch delete and a purge queue the document’s files too. MongoDB’s own retention of a soft-deleted document can’t:backd storage reconcilefinds those objects. - The admin UI gains the file controls and schema checks. Its
Content-Security-Policynow also allows images and direct uploads from the origin of the realms’ file storage (and from no other origin besides its own); a stack without storage has the policy it had. - API and CLI additions: the
_filesroutes and?file_links=trueon reads;GET /_admin/storage,POST /_admin/storage/checkand/reconcile;/_admin/data-checks;/_admin/schedules;backd storage check|usage|reconcile,backd data check|checks,backd functions schedules|pause|resume. New error codes:too_many_files,upload_mode_mismatch,unsupported_file_type,file_missing,invalid_file_link,upload_mismatch,file_not_uploadedandstorage_unavailable. Theownedreport listsreplaceas[], no longernull, for a policy that replaces nothing. - The tutorial’s stack now runs a MinIO: fetch
compose.yamlandnginx.confagain before chapter 13. - The JavaScript client is 0.7.0 (
npm install backd-js@latest):files(id, field),uploadFile,prepareUpload,fileUrl,downloadFile,deleteFile,fileLinks,admin.storage.status()andreconcile(),admin.dataChecks,admin.schedules,job.stepsandwait({ onProgress }). It works with a v0.7.0 server for everything that existed before.
Upgrading to v0.7.0
v0.7.0 adds the admin web interface, a read-only admin level with admin.read_access, an audited admin data route, a read-only configuration view in the admin API, and a switch that turns the admin API off per instance. Configuration that worked on v0.6.0 still loads; the interface is off until you switch it on. What to check:
admin: trueand admin API keys now reach documents. The new admin data route (/v1/{realm}/_admin/data/…) lists, reads and changes documents past the collection’s access rules, and it belongs to everyadmin: truerole and every admin API key. Writes are audited without their content; reads are not (they carry the actor in the access log). Before upgrading, look at who holds those roles and keys: give people who don’t need documents an area list (admin: [users, invitations]), which doesn’t include the newdataarea, and keep admin keys to the tooling that needs them.- Two new areas. An area list can now also name
dataandconfig; a role that lists areas and doesn’t name them doesn’t get them.configshows what the instance runs for the realm (GET /_admin/config) and every read-only level has it. - The read-only level is new.
admin: read(or[read, secrets]) reads every area except users and data, which a realm opts in to withadmin.read_access.usersandadmin.read_access.data(bothfalseby default).GET /_admin/whoamitells a client what the signed-in level may do. - The admin API can be switched off per instance:
BACKD_ADMIN_API=falseleaves every/_adminroute out (404). The production reference now splits a publicbackd(no admin API) from an internalbackd-admin, and the CLI’sBACKD_URLpoints at the internal one: if you built ondeploy/production, compare your compose file with it. - The admin web interface is off by default.
BACKD_ADMIN_UI=trueserves it at/_ui/from an instance that also has the admin API (the combination withBACKD_ADMIN_API=falsestops startup);BACKD_ADMIN_UI_IDLE(default30m) signs an idle administrator out. Release images and binaries embed it; a binary built from source needsmake ui(Node) first, and without it/_ui/answers404and startup warns. Serve it only from the internal instance, and read its security review and the hardening checklist first. - Smaller changes:
GET /_admin/userstakesq(a search of at most 254 characters),GET /_admin/users/{id}/sessionsandDELETE …/sessions/{session_id}list and end a user’s sessions. The example stack serves/_ui/and has anadminuirealm for it. - The JavaScript client is 0.6.0 (
npm install backd-js@latest):admin.whoami(),admin.config(),admin.data(database, collection),admin.users.sessions()andrevokeSession(), theqsearch onadmin.users.list()and theoriginfilter onadmin.jobs.list(). It works with a v0.6.0 server for everything that existed before.
Upgrading to v0.6.0
v0.6.0 adds soft delete, admin rights per role, Prefer: respond-async and login_throttle. Configuration that worked on v0.5.0 still loads and behaves the same; each new feature is off until you use it. What to check:
- Provision before rolling out, as always. A collection with
soft_deletegets its unique indexes rebuilt with_meta.deleted_atas the last key and, with aretention, a TTL index on_meta.purge_at;PROVISION_MODE=verifyrefuses to start untilbackd provisionhas created them. Collections that don’t opt in are untouched. - Turning soft delete on for a collection that already has a unique index: the old index stays (
backdnever drops one) and also covers deleted documents, so a deleted document’s values stay taken until you drop it.backd provisionwarns about it by name; drop it once the new index exists. - Admin roles:
admin: trueis what it was. A role can now list areas (users,invitations,apikeys,secrets,audit,functions) and an administrator can no longer hand out, or change a user who holds, rights they don’t have themselves; admin API keys are created and revoked only by full administrators. Keep one role withadmin: true:backd bootstrapneeds it, and startup warns when a realm has none. restoreandpurgeare new rule keys for collections that soft-delete. They are denied unless declared andwritedoesn’t cover them; declaring one in a collection withoutsoft_deletestops startup.backd rules testfixtures takerestoreandpurgeassertions and_meta.deleted_at.- Two query parameters are now meaningful:
deletedon list and get, andpurgeonDELETE. On a collection that doesn’t soft-delete they still answer400, with a message that says so. Prefer: respond-asyncis per call. Nothing changes for callers who don’t send it. Browser apps on another origin can now sendIdempotency-KeyandPreferand readPreference-AppliedandIdempotent-Replayed: the CORS allow and expose lists lacked them.login_throttleis optional; the defaults are the values the throttle always had.- The JavaScript client is 0.5.0 (
npm install backd-js@latest):deleted,purgeandrestoreon collections,respondAsynconfn(). It works with a v0.5.0 server for everything that existed before.
Upgrading to v0.5.0
v0.5.0 adds cursor pagination, API key scopes, session cookies, Idempotency-Key on creates, dates stored as dates and backd rules test. Configuration that worked on v0.4.0 still loads; every new feature is off until you use it. What to check:
- Provision before rolling out, as always: the
api_keyscollection of each realm’s system database gains thescopesfield in its validator, and each collection with a field marked"x-backd-store": "date"getsbsonType: datefor it.PROVISION_MODE=verifyrefuses to start untilbackd provisionhas run. - API key scopes are opt-in. Existing keys have no scopes and keep full access. A key created with
scopescan only do what they grant (read,writeorcall, on everything, a database or a collection or function), and answers403otherwise. x-backd-store: datedoes not convert existing data. Documents written before keep text in that field until they are written again, and date queries skip them until then; the dates section has amongoshone-liner to convert in one go.- Session cookies are opt-in per realm and per login (
auth.sessions.cookie.enabled, then"cookie": trueon the login). A cookie login turns on an origin check for writes that carry only the cookie, so list your app’s origin incors.origins. Idempotency-Keyon creates needs authentication. A create or batch with the header answers400for anonymous callers and in realms withauth: disabled; without the header nothing changes. Answers are kept for 24 hours in the realm’s existingidempotencycollection.- The admin user list is cut by MongoDB now (
GET /_admin/usersreturnsnext_cursor, withafterto continue;skipstill works). The command line and the JavaScript client already follow it; a script that read the whole list in one call must follownext_cursorfor realms with more users than one page. - Two limits are stricter, one is new. The login throttle now serializes the password checks of an account across instances, and answers
429withRetry-After: 1if it waits more than 5 seconds for the lock.auth.sessions.admin_idle_timeoutandauth.sessions.admin_max_lifetime(unset: nothing changes) shorten the sessions of users who hold an admin role. See the hardening checklist. - The JavaScript client is 0.4.0 (
npm install backd-js@latest):afteranditeratefollow cursors,cookies: true,scopeswhen creating keys, andidempotencyKeyoncreateandbatch(keyed requests are retried by the client). It is versioned apart from the server, and works with a v0.4.0 server for everything that existed before. - If you copied the template workflow (
backd template project→.github/workflows/backd.yml), add a stepbackd rules test(withCONFIG_DIR: ./config) afterbackd config check, as the template now does: it checks your access rules against the cases in eachrules.test.yaml, with no database.
Upgrading to v0.4.0
v0.4.0 adds email flows, erasure, metrics and the npm client. Configuration that worked on v0.3.0 still loads and nothing new is required, but two behaviors change, so read the first two items.
- Deleting a user means something else.
DELETE /_auth/me(a user deleting their own account) now deactivates it: the account is disabled and its sessions end, but the data and the email address are kept, and an administrator can reactivate it.DELETE /_admin/users/{id}andbackd user deletenow erase: the user becomes a tombstone (erased-<id>@erased.invalid, so the real address is free again) and a worker applies thecollection.yamlpolicy of each collection that declares one; they answer202with an erase job, andbackd user deletewaits for it. Before, both removed the user and left every document alone. To get that effect now, declare no policy: an erase without policies leaves the documents as they are. A worker must be running (backd worker, orserve --with-worker); see Deleting and erasing users. - Provision before rolling out, as always: each realm’s system database gains collections and indexes (email tokens, an index of erase jobs), and
PROVISION_MODE=verifyrefuses to start without them. The worker now claims jobs in every realm with authentication, not only those with functions. - Some addresses are refused. Addresses on the reserved
.invaliddomain, with a comma, semicolon, colon, angle bracket, parenthesis, square bracket, quote or backslash, or longer than 254 characters no longer pass, at sign-up and when looked up. An existing account with such an address can’t sign in by email until an administrator changes the address in the database. Ordinary addresses are not affected. - If you copied the production reference: the stricter rate-limit zone now covers password reset, verification, invitations and address changes (
nginx/backd.conf.template), and it gained a Prometheus. Run itssetup.shagain: it addsBACKD_METRICS_TOKENto your.envandsecrets/metrics-token. - The JavaScript client is on npm as
backd-js(npm install backd-js). It was never published as@backd/client: if you installed it from a checkout, change the import. Versions ofbackd-jsbefore 0.2 were an unrelated, older library. - Metrics, email and the
accountsettings are all off by default. Turn them on when you want them:METRICS_ADDR, theemailsection ofrealm.yamlandaccount.*.
Upgrading to v0.2.0
v0.2.0 adds users and access rules and is a breaking release: every realm needs a realm.yaml, and authentication is on by default. To keep an existing realm working as before on a private network, give it a realm.yaml with auth: disabled; to protect it, provision it and give server-side clients API keys.
- Add a
realm.yamlto every realm. To keep a realm working exactly as before, without authentication, putauth: disabledin it. Only do this for realms that stay on a private network: anyone who can reach them can read and modify their data. - Or turn authentication on (an
authsection withstrategies), declare an admin role inrealm.yaml(roles: {admin: {admin: true}}), then:- run
backd provision(or startbackd servewithPROVISION_MODE=apply) to create the realm’s system database; - create the realm’s first administrator with
backd bootstrap --realm <realm> --email <email>, and log in as them withbackd login --realm <realm> --url <server>; - give server-side clients an API key:
backd apikey create --realm <realm> --name <name> --expires 90d, sent asAuthorization: Bearer <key>; - add a
rules:section to thecollection.yamlof collections that users or anonymous callers should reach. Without one, only API keys can.
- run
- Existing documents don’t have ownership fields. They keep working:
_meta.ownerreads asnull, so owner-based rules don’t match them, and their next write adds_meta.updated_by. API keys can still manage them. - Behind a reverse proxy, set
TRUSTED_PROXIESso login throttling sees real client addresses. - Browser apps on another origin need that origin under
cors.originsinrealm.yaml.
Health checks
Use GET /healthz as the liveness probe and GET /readyz as the readiness probe (see HTTP API).
Logging
Logs are JSON lines on standard error, filtered by LOG_LEVEL.
- At startup,
backdlogs aWARNline for each realm withauth: disabled(its data is open to anyone who can reach the service), and for each realm with anauthsection but no rules in any collection (only API keys can reach its data). It warns about realms with anauthsection that nobody can administer with a session (no admin role, or no user holding one). It also lists API keys that never expire or expire within 14 days (see the expiry policy). - Each request produces one access-log line (
"msg":"request"). It includes the method, route pattern, realm, database, collection, status, duration, bytes,request_idand the client’s address (client, followingTRUSTED_PROXIES), plusactorin realms with anauthsection:user:<id>for a session,key:<name>for an API key,key:<name> as user:<id>for an API key acting on behalf of a user, oranonymous. - At
debuglevel, requests refused by access rules are logged ("msg":"access denied") with the collection, operation and reason. - Request bodies, document contents, passwords and tokens are never logged.
Timeouts
| Timeout | Value |
|---|---|
| Storage work per document request | MONGO_OP_TIMEOUT (default 10s). Exceeding it returns 503 unavailable |
| Reading request headers | 10s |
| Reading the whole request | 30s |
| Writing the response | MONGO_OP_TIMEOUT + 10s, at least 30s |
| Idle keep-alive connections | 120s |
Shutdown
On SIGINT or SIGTERM, backd:
- closes its listener at once, so new connections and readiness probes are refused;
- lets in-flight requests finish, for up to
SHUTDOWN_TIMEOUT(default 15s); - closes the MongoDB client and exits with status 0.
Requests still running after the timeout are aborted, and backd exits with a non-zero status.
Give your process manager a stop grace period longer than SHUTDOWN_TIMEOUT:
- the included
docker-compose.ymlusesstop_grace_period: 20s; - Docker’s default is 10s;
- Kubernetes’ default
terminationGracePeriodSecondsis 30s.
backd serves plain HTTP. Terminate TLS in front of it with a reverse proxy, ingress or load balancer. The production reference deployment does this and everything else below. Before going public, go through the hardening checklist. Before exposing a realm, also go through the security checklist.
Rate limiting and abuse
backd doesn’t limit request rates, except for failed logins. Put a reverse proxy, API gateway or CDN in front of it and limit there (the production reference has a tested nginx configuration):
- Sign-up: in realms with
signup: open, limitPOST /v1/{realm}/_auth/signupper client address, or usesignup: invite. - Public data: collections whose
readrule lets anonymous callers in can be scraped; limit or cache them. - Everything else: a general per-address limit protects MongoDB from floods.
Password hashing is bounded by PASSWORD_HASH_CONCURRENCY whatever the traffic: excess logins queue, then answer 503 at their deadline, instead of exhausting memory.
Client addresses behind a proxy
Login throttling counts failures per client address. Behind a proxy, every connection comes from the proxy, so set TRUSTED_PROXIES to the proxies’ exact addresses:
TRUSTED_PROXIES=172.30.80.10
List networks only if every address in them is one of your proxies. Whole private ranges such as 10.0.0.0/8 would let any other container or host in them forge X-Forwarded-For (see client addresses in the production reference).
X-Forwarded-Foris only believed when the connection comes from a trusted proxy. Otherwise clients could pick any address they like.- The client is the right-most address in
X-Forwarded-Forthat isn’t a trusted proxy. Entries further left were supplied by the client and are ignored. - The proxy in front of
backdmust overwriteX-Forwarded-Forwith the address it sees, unless it trusts a load balancer in front of it. The production reference shows how. The local stacks (make dev,make example) trust all private ranges for convenience: don’t copy that. - Without
TRUSTED_PROXIES,backduses the connection’s address. Behind a proxy, all clients then share one counter, and a flood of failures from anyone slows down logins for everyone behind that proxy.
Connect external services to a realm: file storage backends and push notification providers.
AdministrationOperating a realm’s users and data: the admin API and UI, the command line, audit trail, erasure, compliance and the security model.
Deploying configBuild CONFIG_DIR once from a tag of your config repository, deploy the same artifact everywhere, and let backd refuse instances that run anything else.
Production deploymentWhat every internet-facing installation needs, wherever it runs: TLS, rate limits, client addresses, a closed admin API, MongoDB with authentication and least privilege, isolated functions and files. Then how Docker Compose and Kubernetes do it, and where to host.
MetricsPrometheus metrics from every backd process on a private port: how to turn them on, what they hold, and why the port must never be public.
Change feedHow backd reads MongoDB’s change stream for realtime and triggers: when it reads, what it never reads, what it costs and what to size.
Executor, workers and egressThe executor, workers and egress: how to run them.
Keeping storage tidyWhat happens to a file’s object when its document changes or is deleted, erasing users, usage totals, backups, and backd storage usage and reconcile.
Organization sign-in for operatorsThe realm switch, the limits, the secrets and the checks for letting organizations sign in with their own Google Workspace or Microsoft tenant.
Backup and restoreWhat must be backed up, encrypted backups with mongodump and age, restoring, and what to do after a restore.
If credentials leakA runbook for a leaked backup, a credential dump or a stolen key: what to do in the first minutes, in order, what your users will see and how long it takes.
Hardening checklistOne page to go through before a backd deployment faces the internet: realms, credentials, the edge, MongoDB and operations.