<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>backd</title><link>https://fernandezvara.github.io/backd/</link><description>Recent content on backd</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://fernandezvara.github.io/backd/index.xml" rel="self" type="application/rss+xml"/><item><title>Getting started</title><link>https://fernandezvara.github.io/backd/docs/getting-started/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/getting-started/</guid><description>&lt;p&gt;Core concepts:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;realm realm.yaml settings; shared user pool with single sign-on
 └─ database one application&amp;#39;s data → MongoDB database &amp;lt;realm&amp;gt;__&amp;lt;database&amp;gt;
 └─ collection documents validated by schema.json → one MongoDB collection
 └─ document JSON object with server-owned id and _meta
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="quick-start-with-docker-compose"&gt;Quick start with docker compose&lt;a class="hd-anchor" href="#quick-start-with-docker-compose" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The repository includes a &lt;code&gt;docker-compose.yml&lt;/code&gt; that runs &lt;code&gt;backd&lt;/code&gt; and MongoDB with the sample config in &lt;code&gt;examples/config&lt;/code&gt;, together with this documentation site and the JavaScript client&amp;rsquo;s &lt;a href="../examples/"&gt;example apps&lt;/a&gt;. nginx serves all of them over HTTPS on one origin, &lt;code&gt;https://localhost:8443&lt;/code&gt;. Plain &lt;code&gt;http://localhost:8080&lt;/code&gt; redirects there. It also runs the &lt;a href="../functions/"&gt;functions&lt;/a&gt; executor, egress and (folded into &lt;code&gt;backd&lt;/code&gt;) a worker: the blog realm&amp;rsquo;s sample &lt;code&gt;stats&lt;/code&gt; function calls the executor for real (&lt;code&gt;curl https://localhost:8443/v1/blog/main/_func/stats&lt;/code&gt;).&lt;/p&gt;</description></item><item><title>1. An empty shelf</title><link>https://fernandezvara.github.io/backd/docs/tutorial/01-empty-shelf/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/01-empty-shelf/</guid><description>&lt;p&gt;A collection, a schema, and a page that lists documents. No users yet: anyone on localhost can read and write, which is exactly what you want while the shape of the data settles.&lt;/p&gt;
&lt;aside class="hd-hint hd-hint--warning"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;path d="M12 3 2 21h20Z"/&gt;&lt;path d="M12 10v4m0 3h.01"/&gt;&lt;/svg&gt;
Warning&lt;/p&gt;
 &lt;code&gt;auth: disabled&lt;/code&gt; means anyone who can reach backd can read and modify its data. Keep this stack on localhost — chapter 2 turns authentication on and &lt;a href="../../operations/checklist/"&gt;the checklist&lt;/a&gt; is what you run before anything faces the internet.
&lt;/aside&gt;

&lt;h2 id="the-realm"&gt;The realm&lt;a class="hd-anchor" href="#the-realm" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A &lt;strong&gt;realm&lt;/strong&gt; is one application&amp;rsquo;s world: its users, roles and settings. A &lt;strong&gt;database&lt;/strong&gt; inside it groups collections (&lt;code&gt;main&lt;/code&gt; here), and a &lt;strong&gt;collection&lt;/strong&gt; holds documents — the vocabulary the URLs use: &lt;code&gt;/v1/{realm}/{database}/{collection}&lt;/code&gt;. They are directories under &lt;code&gt;config/&lt;/code&gt;. Create &lt;code&gt;config/shelf/realm.yaml&lt;/code&gt;:&lt;/p&gt;</description></item><item><title>Data model (CONFIG_DIR)</title><link>https://fernandezvara.github.io/backd/docs/configuration/config-dir/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/configuration/config-dir/</guid><description>&lt;p&gt;The data model is a directory tree under &lt;code&gt;CONFIG_DIR&lt;/code&gt;, loaded once at startup. Directories define realms and databases, and a &lt;code&gt;schema.json&lt;/code&gt; defines a collection.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$CONFIG_DIR/&amp;lt;realm&amp;gt;/
 realm.yaml # required — realm settings
 email/&amp;lt;kind&amp;gt;/ # optional — email templates (see Functions → Email)
 &amp;lt;database&amp;gt;/&amp;lt;collection&amp;gt;/
 schema.json # required — JSON Schema draft 2020-12
 indexes.json # optional — index declarations
 collection.yaml # optional — access rules, file fields, soft delete, erase policy
 rules.test.yaml # optional — tests of the rules (`backd rules test`)
 &amp;lt;database&amp;gt;/_functions/ # optional — server-side functions
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Each directory under &lt;code&gt;CONFIG_DIR&lt;/code&gt; is a &lt;strong&gt;realm&lt;/strong&gt;. It must contain a &lt;a href="../realm/"&gt;&lt;code&gt;realm.yaml&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Each directory inside a realm is a &lt;strong&gt;database&lt;/strong&gt;, stored in the MongoDB database &lt;code&gt;&amp;lt;realm&amp;gt;__&amp;lt;database&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each directory inside a database is a &lt;strong&gt;collection&lt;/strong&gt;, stored in a MongoDB collection of the same name. It must contain a &lt;code&gt;schema.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A realm&amp;rsquo;s &lt;code&gt;email&lt;/code&gt; folder holds its &lt;a href="../../functions/email/#templates"&gt;email templates&lt;/a&gt;, and &lt;code&gt;pages&lt;/code&gt; is reserved for the pages backd will serve; neither is a database, so they are never read as one and can&amp;rsquo;t be database names.&lt;/li&gt;
&lt;li&gt;A database&amp;rsquo;s &lt;code&gt;_functions&lt;/code&gt; directory holds its &lt;a href="../../functions/"&gt;functions&lt;/a&gt;, not a collection.&lt;/li&gt;
&lt;li&gt;Other plain files at the realm and database levels are ignored, as are hidden entries (names starting with &lt;code&gt;.&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Changing the config requires a restart. There is no hot-reload.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="starting-from-a-template"&gt;Starting from a template&lt;a class="hd-anchor" href="#starting-from-a-template" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;backd template&lt;/code&gt; writes commented starter files, so you don&amp;rsquo;t have to start from an empty directory. It only needs &lt;code&gt;CONFIG_DIR&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Realm settings (realm.yaml)</title><link>https://fernandezvara.github.io/backd/docs/configuration/realm/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/configuration/realm/</guid><description>&lt;p&gt;Every realm directory must contain a &lt;code&gt;realm.yaml&lt;/code&gt;. It holds the settings that apply to the whole realm: its users, how they sign in, and which browser origins may call it.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$CONFIG_DIR/&amp;lt;realm&amp;gt;/realm.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If a realm has no &lt;code&gt;realm.yaml&lt;/code&gt;, &lt;code&gt;backd&lt;/code&gt; refuses to start and names the realm. Create a commented starter file with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./config backd template realm --realm &amp;lt;realm&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="example"&gt;Example&lt;a class="hd-anchor" href="#example" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;invite&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;idle_timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;14d&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_lifetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;60d&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;min_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;https://app.example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;http://localhost:5173&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Manages users and invitations&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;ops@example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lead@example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Can publish articles&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;An empty file, or one with only comments, is valid: every key has a default.&lt;/p&gt;</description></item><item><title>2. Members</title><link>https://fernandezvara.github.io/backd/docs/tutorial/02-members/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/02-members/</guid><description>&lt;p&gt;Chapter 1&amp;rsquo;s realm was open to anyone on localhost. Now members sign up and sign in — and the app learns who you are: your gallery, your assets, your account page.&lt;/p&gt;
&lt;h2 id="the-realm-grows"&gt;The realm grows&lt;a class="hd-anchor" href="#the-realm-grows" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Replace &lt;code&gt;config/shelf/realm.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;enabled&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;open&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Operators: users, invitations and API keys&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;operator@shelf.example&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;curator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Publishes assets and manages share links (chapter 5)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;curator@shelf.example&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;auth: enabled&lt;/code&gt;&lt;/strong&gt; — users, sessions and roles exist; anonymous callers get &lt;code&gt;401&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;signup: open&lt;/code&gt;&lt;/strong&gt; — anyone may create an account (&lt;a href="../../auth/users/"&gt;alternatives&lt;/a&gt;: &lt;code&gt;invite&lt;/code&gt;, &lt;code&gt;closed&lt;/code&gt;). It is open &lt;strong&gt;only for this chapter and the next&lt;/strong&gt;: chapter 4 closes it, and from then on every new member needs an invitation — which only an existing administrator can create.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;roles:&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;admin&lt;/code&gt; is the operator role (&lt;code&gt;admin: true&lt;/code&gt; grants the admin API); &lt;code&gt;curator&lt;/code&gt; matters from chapter 5. The &lt;code&gt;users:&lt;/code&gt; lists are &lt;strong&gt;seed assignments&lt;/strong&gt;: when someone signs up with a listed address, the role attaches. A shortcut for local stacks — never keep seeded users in a real realm.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="apply-and-create-the-three-accounts"&gt;Apply, and create the three accounts&lt;a class="hd-anchor" href="#apply-and-create-the-three-accounts" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose restart backd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Open &lt;code&gt;http://localhost:8080&lt;/code&gt; and &lt;strong&gt;sign up&lt;/strong&gt; (any password of 12 or more characters; the demo stack sends no email yet and &lt;code&gt;account.require_verified_email&lt;/code&gt; is off, so nothing needs verifying — the &lt;code&gt;verify-email&lt;/code&gt; and &lt;code&gt;welcome&lt;/code&gt; messages only appear once chapter 10 wires email) as each of:&lt;/p&gt;</description></item><item><title>Schema validation</title><link>https://fernandezvara.github.io/backd/docs/configuration/validation/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/configuration/validation/</guid><description>&lt;p&gt;Every collection&amp;rsquo;s &lt;code&gt;schema.json&lt;/code&gt; is enforced at two levels.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;API level (authoritative).&lt;/strong&gt; Every write is validated against the full JSON Schema draft 2020-12 before it reaches the database. Failures return &lt;code&gt;400&lt;/code&gt; with one entry per failing field.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database level (safety net).&lt;/strong&gt; The same schema is translated into a MongoDB &lt;code&gt;$jsonSchema&lt;/code&gt; collection validator. This protects the data from writes that bypass the API. MongoDB&amp;rsquo;s dialect is older and smaller, so the translation covers a subset of the schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="translation-to-mongodb"&gt;Translation to MongoDB&lt;a class="hd-anchor" href="#translation-to-mongodb" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;JSON Schema&lt;/th&gt;
 &lt;th&gt;MongoDB &lt;code&gt;$jsonSchema&lt;/code&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;type: string&lt;/code&gt; / &lt;code&gt;boolean&lt;/code&gt; / &lt;code&gt;object&lt;/code&gt; / &lt;code&gt;array&lt;/code&gt; / &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;bsonType: string&lt;/code&gt; / &lt;code&gt;bool&lt;/code&gt; / &lt;code&gt;object&lt;/code&gt; / &lt;code&gt;array&lt;/code&gt; / &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;type: integer&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;bsonType: [int, long, double]&lt;/code&gt; plus &lt;code&gt;multipleOf: 1&lt;/code&gt;, unless the schema sets its own &lt;code&gt;multipleOf&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;type: number&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;bsonType: [int, long, double, decimal]&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;const: v&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;enum: [v]&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;exclusiveMinimum: n&lt;/code&gt; / &lt;code&gt;exclusiveMaximum: n&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;minimum&lt;/code&gt; / &lt;code&gt;maximum: n&lt;/code&gt; plus &lt;code&gt;exclusiveMinimum&lt;/code&gt; / &lt;code&gt;exclusiveMaximum: true&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;properties&lt;/code&gt;, &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;, &lt;code&gt;minimum&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, &lt;code&gt;multipleOf&lt;/code&gt;, &lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;maxLength&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt;, &lt;code&gt;minItems&lt;/code&gt;, &lt;code&gt;maxItems&lt;/code&gt;, &lt;code&gt;uniqueItems&lt;/code&gt;, &lt;code&gt;additionalProperties&lt;/code&gt;, &lt;code&gt;minProperties&lt;/code&gt;, &lt;code&gt;maxProperties&lt;/code&gt;, &lt;code&gt;allOf&lt;/code&gt;, &lt;code&gt;anyOf&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;, &lt;code&gt;not&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;copied as-is (subschemas translated recursively)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;Annotations (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;examples&lt;/code&gt;, &lt;code&gt;$comment&lt;/code&gt;, …) are dropped silently.&lt;/li&gt;
&lt;li&gt;Any other keyword (for example &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;$ref&lt;/code&gt;/&lt;code&gt;$defs&lt;/code&gt;, &lt;code&gt;prefixItems&lt;/code&gt;, &lt;code&gt;unevaluatedProperties&lt;/code&gt; or &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;then&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt;) is left out of the database validator and logged at &lt;code&gt;info&lt;/code&gt; level with its location. The API level still enforces it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="finding-documents-that-no-longer-match"&gt;Finding documents that no longer match&lt;a class="hd-anchor" href="#finding-documents-that-no-longer-match" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A schema is enforced when a document is &lt;strong&gt;written&lt;/strong&gt;. Documents stored before you changed the schema are not touched, and MongoDB is set to leave them alone (&lt;code&gt;validationLevel: moderate&lt;/code&gt;): a collection can hold documents that would be refused today. A &lt;em&gt;schema check&lt;/em&gt; lists them. It reads every stored document, validates it with the same full validator the API uses on a write (so it also sees the keywords MongoDB&amp;rsquo;s translation leaves out, such as &lt;code&gt;format&lt;/code&gt;), and reports the ones that fail. It &lt;strong&gt;never writes&lt;/strong&gt; anything.&lt;/p&gt;</description></item><item><title>3. Who may touch what</title><link>https://fernandezvara.github.io/backd/docs/tutorial/03-rules/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/03-rules/</guid><description>&lt;p&gt;Until now every signed-in member could do anything to anything. The &lt;code&gt;rules:&lt;/code&gt; section of a collection&amp;rsquo;s &lt;code&gt;collection.yaml&lt;/code&gt; gives it four expressions — &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; — evaluated per call. Operations without a rule are denied.&lt;/p&gt;
&lt;h2 id="the-assets-rules"&gt;The assets rules&lt;a class="hd-anchor" href="#the-assets-rules" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Replace &lt;code&gt;config/shelf/main/assets/collection.yaml&lt;/code&gt; (chapter 2&amp;rsquo;s policy stays; the rules go above it):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Everyone in the workspace reads published assets; a member also reads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# their own drafts.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; user != nil
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;amp;&amp;amp; (document.published_at != nil || document._meta.owner == user.id)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Members write their own assets. HOLE: published_at is the client&amp;#39;s word —&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# any member can publish, and pick the date.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user != nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user != nil &amp;amp;&amp;amp; document._meta.owner == user.id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user != nil &amp;amp;&amp;amp; document._meta.owner == user.id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on_owner_delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;delete&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three names carry the context: &lt;code&gt;user&lt;/code&gt; (the caller, or &lt;code&gt;nil&lt;/code&gt; when anonymous — always check &lt;code&gt;user != nil&lt;/code&gt; first), &lt;code&gt;document&lt;/code&gt; (the stored asset), &lt;code&gt;data&lt;/code&gt; (the asset being written). &lt;code&gt;changed()&lt;/code&gt; lists the fields a write touches, and &lt;code&gt;hasRole(user, 'curator')&lt;/code&gt; checks a &lt;code&gt;realm.yaml&lt;/code&gt; role. The full language is in &lt;a href="../../auth/rules/"&gt;Access rules&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>4. Invitations</title><link>https://fernandezvara.github.io/backd/docs/tutorial/04-invitations/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/04-invitations/</guid><description>&lt;p&gt;A team&amp;rsquo;s library shouldn&amp;rsquo;t be open to whoever finds it. One word in &lt;code&gt;realm.yaml&lt;/code&gt; closes the door; invitations are how people get in anyway.&lt;/p&gt;
&lt;h2 id="close-the-door"&gt;Close the door&lt;a class="hd-anchor" href="#close-the-door" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Before you change anything, check the three accounts of chapter 2 exist: &lt;strong&gt;operator, curator and your member&lt;/strong&gt;. Then, in &lt;code&gt;config/shelf/realm.yaml&lt;/code&gt;, change one line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;signup: invite # was&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;open&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and &lt;code&gt;docker compose restart backd&lt;/code&gt;. Existing accounts keep working — this only governs &lt;strong&gt;new&lt;/strong&gt; sign-ups. From now on &lt;code&gt;POST /_auth/signup&lt;/code&gt; without a valid &lt;code&gt;invitation&lt;/code&gt; token answers &lt;code&gt;403&lt;/code&gt;:&lt;/p&gt;</description></item><item><title>5. The first function</title><link>https://fernandezvara.github.io/backd/docs/tutorial/05-first-function/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/05-first-function/</guid><description>&lt;p&gt;Chapter 3 left two &lt;code&gt;HOLE&lt;/code&gt; comments: members could write &lt;code&gt;published_at&lt;/code&gt; themselves, and share links trusted the client&amp;rsquo;s &lt;code&gt;asset_id&lt;/code&gt; and &lt;code&gt;token&lt;/code&gt;. Rules can&amp;rsquo;t fix either — &amp;ldquo;a curator, at the server&amp;rsquo;s now&amp;rdquo; and &amp;ldquo;a token nobody chose&amp;rdquo; need code. This chapter writes it.&lt;/p&gt;
&lt;h2 id="a-function-is-three-files"&gt;A function is three files&lt;a class="hd-anchor" href="#a-function-is-three-files" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;config/shelf/main/_functions/publish/&lt;/code&gt; with three files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;function.yaml&lt;/code&gt;&lt;/strong&gt; — the contract: who may call it (&lt;code&gt;invoke&lt;/code&gt;), what it may do (&lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;calls&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;secrets&lt;/code&gt;), how it runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/shelf/main/_functions/publish/function.yaml&lt;/code&gt;&lt;/p&gt;</description></item><item><title>6. Reaching the outside</title><link>https://fernandezvara.github.io/backd/docs/tutorial/06-network/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/06-network/</guid><description>&lt;p&gt;Functions run in a sandbox with &lt;strong&gt;no network by default&lt;/strong&gt; — &lt;code&gt;fetch&lt;/code&gt; to anywhere is refused before a packet leaves. A function that needs the outside declares its hosts in &lt;code&gt;function.yaml&lt;/code&gt;, and every request exits through the egress proxy that enforces the list.&lt;/p&gt;
&lt;h2 id="preview"&gt;preview&lt;a class="hd-anchor" href="#preview" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Pasting a link into the new-asset form is a chore; &lt;code&gt;preview&lt;/code&gt; fetches the page&amp;rsquo;s title and description so the form can prefill itself:&lt;/p&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/shelf/main/_functions/preview/function.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Fetches a link&amp;#39;s title and description for the new-asset form. This is the&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# first function that reaches outside: `network` is the whole allowlist —&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# api.microlink.io answers, every other host is refused at the egress. It&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# touches no collection, so no `admin:` either.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;user != nil&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;api.microlink.io]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Two things to notice:&lt;/p&gt;</description></item><item><title>7. Functions calling functions</title><link>https://fernandezvara.github.io/backd/docs/tutorial/07-calls/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/07-calls/</guid><description>&lt;p&gt;&lt;code&gt;publish&lt;/code&gt; works. Now the owner should hear about it — and the same &amp;ldquo;write a notification&amp;rdquo; will soon be wanted by the digest, the cleanup, the importer. That&amp;rsquo;s a building block, not an endpoint. &lt;code&gt;internal: true&lt;/code&gt; makes it one.&lt;/p&gt;
&lt;h2 id="the-notifications-collection"&gt;The notifications collection&lt;a class="hd-anchor" href="#the-notifications-collection" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A member reads their own notifications and can flip exactly one field:&lt;/p&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/shelf/main/notifications/collection.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Notifications are written by functions only — there is no create rule.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# A member reads their own and may set exactly one field, read_at.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user != nil &amp;amp;&amp;amp; document.to_user == user.id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; user != nil &amp;amp;&amp;amp; document.to_user == user.id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; &amp;amp;&amp;amp; all(changed(), # in [&amp;#39;read_at&amp;#39;])&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There is deliberately no &lt;code&gt;create&lt;/code&gt; rule — nobody may write a notification directly. The only writer is a function.&lt;/p&gt;</description></item><item><title>8. Work that takes time</title><link>https://fernandezvara.github.io/backd/docs/tutorial/08-async/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/08-async/</guid><description>&lt;p&gt;Fanning a notification out to every member doesn&amp;rsquo;t belong inside an HTTP request. &lt;code&gt;mode: async&lt;/code&gt; moves it to a job: the call returns &lt;code&gt;202&lt;/code&gt; with a job id immediately, the work runs on the executor, and the caller watches it finish.&lt;/p&gt;
&lt;h2 id="the-function"&gt;The function&lt;a class="hd-anchor" href="#the-function" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;config/shelf/main/_functions/digest/&lt;/code&gt;. This chapter&amp;rsquo;s &lt;code&gt;function.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;async&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;user != nil &amp;amp;&amp;amp; hasRole(user, &amp;#39;admin&amp;#39;)&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;notify]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;attempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;5s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_backoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1m&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(The repository&amp;rsquo;s file also has &lt;code&gt;schedule:&lt;/code&gt; and &lt;code&gt;email: true&lt;/code&gt; — chapters 9 and 10 add them.)&lt;/p&gt;</description></item><item><title>9. On a schedule</title><link>https://fernandezvara.github.io/backd/docs/tutorial/09-schedules/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/09-schedules/</guid><description>&lt;p&gt;Some work shouldn&amp;rsquo;t wait for a person: the morning digest, the nightly sweep. One line in &lt;code&gt;function.yaml&lt;/code&gt; and the function runs itself.&lt;/p&gt;
&lt;h2 id="schedule"&gt;schedule&lt;a class="hd-anchor" href="#schedule" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# digest/function.yaml — the same function as chapter 8, plus:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;@daily&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cron expressions work too (&lt;code&gt;&amp;quot;0 7 * * *&amp;quot;&lt;/code&gt;, UTC unless you add a &lt;code&gt;timezone&lt;/code&gt;). A scheduled run has &lt;strong&gt;no caller&lt;/strong&gt;: &lt;code&gt;ctx.user&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;ctx.input&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;ctx.db&lt;/code&gt; acts as an anonymous caller — and &lt;code&gt;ctx.admin.db&lt;/code&gt; is &lt;em&gt;always&lt;/em&gt; available, recorded as &lt;code&gt;func:shelf/main/digest&lt;/code&gt;. Review a scheduled function as if it declared &lt;code&gt;admin: true&lt;/code&gt; (&lt;a href="../../functions/cron/"&gt;Cron&lt;/a&gt;).&lt;/p&gt;</description></item><item><title>10. Email for real</title><link>https://fernandezvara.github.io/backd/docs/tutorial/10-email/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/10-email/</guid><description>&lt;p&gt;Invitations emailed themselves? Not yet — chapter 4 handed you a token to pass along. This chapter gives the realm an email pipeline: a delivery function, templates for every flow backd sends, hosted pages the links open, and one custom kind of your own.&lt;/p&gt;
&lt;h2 id="the-realm-side"&gt;The realm side&lt;a class="hd-anchor" href="#the-realm-side" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Add to &lt;code&gt;config/shelf/realm.yaml&lt;/code&gt; (keeping &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;signup&lt;/code&gt; and &lt;code&gt;roles&lt;/code&gt; as they are):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mail/email-capture&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Shelf &amp;lt;no-reply@shelf.example&amp;gt;&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allowed_redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;http://localhost:8080]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;redirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;verify_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://localhost:8080/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reset_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://localhost:8080/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;change_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://localhost:8080/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;invitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;http://localhost:8080/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;allow_email_change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;function:&lt;/code&gt;&lt;/strong&gt; — one function (in the new &lt;code&gt;mail&lt;/code&gt; database) delivers every email &lt;code&gt;backd&lt;/code&gt; renders. &lt;code&gt;email-capture&lt;/code&gt; stores them in the &lt;code&gt;mail/outbox&lt;/code&gt; collection instead of sending — &lt;code&gt;dev_only: true&lt;/code&gt;, it only runs under &lt;code&gt;BACKD_DEV&lt;/code&gt;, which the tutorial&amp;rsquo;s &lt;code&gt;compose.yaml&lt;/code&gt; sets (a real deployment doesn&amp;rsquo;t, and &lt;code&gt;backd&lt;/code&gt; then refuses to start with it configured) (&lt;a href="../../functions/email/"&gt;Email&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Templates&lt;/strong&gt; — &lt;code&gt;email/&amp;lt;kind&amp;gt;/&amp;lt;locale&amp;gt;.{subject.txt,txt,html}&lt;/code&gt;, one folder per kind &lt;code&gt;backd&lt;/code&gt; sends (verify-email, reset-password, invitation, welcome, account-exists, change-email, email-changed, password-changed) plus custom kinds like &lt;code&gt;shelf-digest&lt;/code&gt;. Every locale you list needs every kind — startup fails naming what&amp;rsquo;s missing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pages&lt;/strong&gt; — &lt;code&gt;pages/&amp;lt;kind&amp;gt;/&amp;lt;locale&amp;gt;.html&lt;/code&gt;: the hosted pages the email links open. &lt;code&gt;GET&lt;/code&gt; shows a form, &lt;code&gt;POST&lt;/code&gt; consumes the token — mail scanners can never spend your links.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;allowed_redirects&lt;/code&gt; / &lt;code&gt;redirects&lt;/code&gt;&lt;/strong&gt; — after a flow&amp;rsquo;s page, the reader lands back at the app, and only at origins you list.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On a checkout, &lt;code&gt;backd template realm --realm shelf&lt;/code&gt; writes all of &lt;code&gt;email/&lt;/code&gt; and &lt;code&gt;pages/&lt;/code&gt; for you, and &lt;code&gt;backd template email-capture --realm shelf --database mail&lt;/code&gt; the delivery function and &lt;code&gt;outbox&lt;/code&gt;; the tutorial&amp;rsquo;s &lt;code&gt;config/&lt;/code&gt; is mounted read-only into the container, so fetch the finished files instead:&lt;/p&gt;</description></item><item><title>11. Being called by the internet</title><link>https://fernandezvara.github.io/backd/docs/tutorial/11-webhooks/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/11-webhooks/</guid><description>&lt;p&gt;So far everything called your functions &lt;em&gt;for you&lt;/em&gt; — a page, a schedule, another function. A webhook reverses it: a partner feed pushes assets into the shelf, signed in to nobody. The function is the whole trust boundary.&lt;/p&gt;
&lt;h2 id="the-function"&gt;The function&lt;a class="hd-anchor" href="#the-function" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/shelf/main/_functions/import/function.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# A webhook: a partner feed pushes assets into the shelf. The sender has no&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# session, so invoke allows anonymous calls and the function itself is the&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# only check — it verifies the signature before anything else.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# `admin: true` because creating assets is a member&amp;#39;s right, not a stranger&amp;#39;s.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# The signing secret is set with&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# backd secret set --realm shelf --database main --name IMPORT_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;webhook&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;IMPORT_WEBHOOK_SECRET]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mode: webhook&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;ctx.input&lt;/code&gt; is gone; &lt;code&gt;ctx.request&lt;/code&gt; holds the raw &lt;code&gt;{ body, headers }&lt;/code&gt;, and the function returns &lt;code&gt;{ status, body }&lt;/code&gt; itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;invoke: &amp;quot;true&amp;quot;&lt;/code&gt;&lt;/strong&gt; — anonymous calls must reach it; the sender has no session and never will.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets:&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;IMPORT_WEBHOOK_SECRET&lt;/code&gt; is shared with the sender out of band, stored per-database, never in git.&lt;/li&gt;
&lt;/ul&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/shelf/main/_functions/import/index.ts&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Documents</title><link>https://fernandezvara.github.io/backd/docs/api/documents/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/api/documents/</guid><description>&lt;p&gt;Every configured collection gets the same set of endpoints:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Method&lt;/th&gt;
 &lt;th&gt;Path&lt;/th&gt;
 &lt;th&gt;Behavior&lt;/th&gt;
 &lt;th&gt;Success&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Create a document&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;201&lt;/code&gt; + &lt;code&gt;Location&lt;/code&gt; + &lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;List documents&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}/{id}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Fetch one document&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt; + &lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}/{id}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Replace the whole document&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt; + &lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}/{id}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Partial update (&lt;a href="https://www.rfc-editor.org/rfc/rfc7396"&gt;JSON Merge Patch&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt; + &lt;code&gt;ETag&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/{collection}/{id}&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Delete the document&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;204&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/v1/{realm}/{database}/_batch&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;a href="#batch-writes"&gt;Create, replace, patch and delete atomically&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;An unknown realm, database, collection or &lt;code&gt;id&lt;/code&gt; returns &lt;code&gt;404 not_found&lt;/code&gt;. PUT and PATCH never create documents: there are no upserts, so create with POST and let the server choose the &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In a realm with &lt;code&gt;auth: enabled&lt;/code&gt;, requests are decided by the collection&amp;rsquo;s &lt;a href="../../auth/rules/"&gt;access rules&lt;/a&gt;, or allowed outright with an &lt;a href="../../auth/api-keys/"&gt;API key&lt;/a&gt; in &lt;code&gt;Authorization: Bearer …&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Request bodies must be a single JSON object. Anything else returns &lt;code&gt;400 invalid_json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="document-shape"&gt;Document shape&lt;a class="hd-anchor" href="#document-shape" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Documents are returned as bare JSON objects:&lt;/p&gt;</description></item><item><title>12. Running it</title><link>https://fernandezvara.github.io/backd/docs/tutorial/12-running/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/12-running/</guid><description>&lt;p&gt;The shelf works. The last chapter is about &lt;em&gt;operating&lt;/em&gt; it: who acts without a session, who did what, and what to review before a real deployment.&lt;/p&gt;
&lt;h2 id="api-keys"&gt;API keys&lt;a class="hd-anchor" href="#api-keys" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;(This chapter needs the Admin view, so sign in as &lt;code&gt;operator@shelf.example&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;Sessions are for people; &lt;strong&gt;API keys are for machines&lt;/strong&gt; — a CI job that reads the gallery, a monitor that calls a function. In the Admin view, type a name (say &lt;code&gt;ci-reader&lt;/code&gt;) and press &lt;em&gt;Create read:main key&lt;/em&gt;: it makes one scoped to read-only on &lt;code&gt;main&lt;/code&gt;, expiring in 90 days:&lt;/p&gt;</description></item><item><title>Querying</title><link>https://fernandezvara.github.io/backd/docs/api/querying/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/api/querying/</guid><description>&lt;p&gt;The list endpoint, &lt;code&gt;GET /v1/{realm}/{database}/{collection}&lt;/code&gt;, accepts these query parameters:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Parameter&lt;/th&gt;
 &lt;th&gt;Default&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;where&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;—&lt;/td&gt;
 &lt;td&gt;JSON object with filter conditions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;order_by&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Comma-separated fields; prefix a field with &lt;code&gt;-&lt;/code&gt; for descending&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;20&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Page size, &lt;code&gt;1&lt;/code&gt;–&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;skip&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Number of documents to skip&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;after&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;The &lt;code&gt;next_cursor&lt;/code&gt; of the previous page: continues the list after it (see &lt;a href="#paging-with-a-cursor"&gt;Paging with a cursor&lt;/a&gt;). Can&amp;rsquo;t be combined with &lt;code&gt;skip&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;count&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;true&lt;/code&gt; adds &lt;code&gt;total&lt;/code&gt;, the number of documents matching &lt;code&gt;where&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;deleted&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;In a collection with &lt;a href="../documents/#soft-delete"&gt;soft delete&lt;/a&gt;: &lt;code&gt;only&lt;/code&gt; lists the deleted documents (the trash), &lt;code&gt;include&lt;/code&gt; both. Needs the &lt;code&gt;restore&lt;/code&gt; rule besides &lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each parameter may appear at most once. Any other parameter returns &lt;code&gt;400 invalid_query&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>13. Files on assets</title><link>https://fernandezvara.github.io/backd/docs/tutorial/13-files/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/13-files/</guid><description>&lt;p&gt;Until now an asset was a link or a note. Real shelves hold files: a PDF handbook, a picture of the office. &lt;code&gt;backd&lt;/code&gt; keeps no files itself: a realm brings &lt;strong&gt;its own S3-compatible storage&lt;/strong&gt;, and the tutorial&amp;rsquo;s stack gains a MinIO to be it. Every file belongs to a field of a document and is as private as that document: reading a file is reading the asset, and every upload or removal is an update checked &lt;strong&gt;before any byte is stored&lt;/strong&gt;.&lt;/p&gt;</description></item><item><title>14. Big files and thumbnails</title><link>https://fernandezvara.github.io/backd/docs/tutorial/14-big-files/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/14-big-files/</guid><description>&lt;p&gt;A 25 MiB limit and a trip through &lt;code&gt;backd&lt;/code&gt; suit a handbook, not a conference recording. This chapter adds &lt;strong&gt;attachments&lt;/strong&gt;: several files, up to a gigabyte each, that the browser sends &lt;strong&gt;straight to the bucket&lt;/strong&gt;. Then a worker makes a small picture of each image, declared in one line.&lt;/p&gt;
&lt;h2 id="attachments-straight-to-the-storage"&gt;Attachments, straight to the storage&lt;a class="hd-anchor" href="#attachments-straight-to-the-storage" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;attachments&lt;/code&gt; was declared with &lt;code&gt;files:&lt;/code&gt; in the last chapter. What makes it different is one line, &lt;code&gt;upload: direct&lt;/code&gt;:&lt;/p&gt;</description></item><item><title>15. Sharing and counting downloads</title><link>https://fernandezvara.github.io/backd/docs/tutorial/15-sharing-files/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/tutorial/15-sharing-files/</guid><description>&lt;p&gt;Shelf can hold files; this chapter makes them travel. Members will see &lt;strong&gt;how often&lt;/strong&gt; an asset&amp;rsquo;s files were downloaded, a stranger holding a share link can download without an account, and the operator can see what it all costs.&lt;/p&gt;
&lt;h2 id="counting-downloads"&gt;Counting downloads&lt;a class="hd-anchor" href="#counting-downloads" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A plain download link tells &lt;code&gt;backd&lt;/code&gt; nothing: the browser fetches the file from the storage. To count, the app asks a &lt;strong&gt;function&lt;/strong&gt; for the link, and the function counts. That is the whole trick: a function is called with &lt;code&gt;POST&lt;/code&gt; and answers JSON, so it can&amp;rsquo;t stream a file itself. It returns the link, and the app opens it.&lt;/p&gt;</description></item><item><title>Deploying config</title><link>https://fernandezvara.github.io/backd/docs/operations/deploying/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/operations/deploying/</guid><description>&lt;p&gt;Everything &lt;code&gt;backd&lt;/code&gt; serves comes from &lt;code&gt;CONFIG_DIR&lt;/code&gt;: realms, schemas, indexes, access rules and &lt;a href="../../functions/"&gt;function&lt;/a&gt; code. Every instance of a deployment must run exactly the same config. If two instances ran different rules or different function code, the same request could be allowed on one and refused on the other.&lt;/p&gt;
&lt;p&gt;So the config is treated like code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it lives in &lt;strong&gt;one config repository&lt;/strong&gt; per deployment;&lt;/li&gt;
&lt;li&gt;CI builds it at &lt;strong&gt;one pinned tag&lt;/strong&gt; into an &lt;strong&gt;immutable artifact&lt;/strong&gt;: an image with &lt;code&gt;backd&lt;/code&gt; and the config inside;&lt;/li&gt;
&lt;li&gt;staging and production run that same artifact;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;backd&lt;/code&gt; checks it (see &lt;a href="#what-backd-enforces"&gt;what backd enforces&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-config-repository"&gt;The config repository&lt;a class="hd-anchor" href="#the-config-repository" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;backd template project --dir &amp;lt;directory&amp;gt; --realm &amp;lt;realm&amp;gt;&lt;/code&gt; scaffolds this layout in one step — a sample database and function with tests, both Dockerfiles, &lt;code&gt;compose.yaml&lt;/code&gt; and the CI workflow below — so you don&amp;rsquo;t have to assemble it by hand; see &lt;a href="../../configuration/config-dir/#starting-from-a-template"&gt;Starting from a template&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Production deployment</title><link>https://fernandezvara.github.io/backd/docs/operations/production/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/operations/production/</guid><description>&lt;p&gt;&lt;code&gt;backd&lt;/code&gt; deliberately leaves some protections to the infrastructure around it: TLS, request rate limits and sign-up spam, body size limits at the edge, and database security. The repository ships a reference deployment that puts all of these in place. It lives in &lt;a href="https://github.com/fernandezvara/backd/tree/main/deploy/production"&gt;&lt;code&gt;deploy/production/&lt;/code&gt;&lt;/a&gt;, and CI tests it end to end on every change.&lt;/p&gt;
&lt;p&gt;It uses Docker Compose, but each part maps directly onto other platforms: a Kubernetes ingress, a cloud load balancer, a managed MongoDB.&lt;/p&gt;</description></item><item><title>Metrics</title><link>https://fernandezvara.github.io/backd/docs/operations/metrics/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/operations/metrics/</guid><description>&lt;p&gt;Every &lt;code&gt;backd&lt;/code&gt; process can tell &lt;a href="https://prometheus.io/"&gt;Prometheus&lt;/a&gt; how it is doing: requests, errors, database latency and, as the next steps land, functions, jobs and email. It is &lt;strong&gt;off by default&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="turning-it-on"&gt;Turning it on&lt;a class="hd-anchor" href="#turning-it-on" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Variable&lt;/th&gt;
 &lt;th&gt;Default&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;METRICS_ADDR&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;empty (off)&lt;/td&gt;
 &lt;td&gt;Where this process serves &lt;code&gt;/metrics&lt;/code&gt;, for example &lt;code&gt;:9090&lt;/code&gt;. A listener of its own: it can&amp;rsquo;t be &lt;code&gt;HTTP_ADDR&lt;/code&gt; or &lt;code&gt;BACKD_INTERNAL_ADDR&lt;/code&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;METRICS_TOKEN&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;empty&lt;/td&gt;
 &lt;td&gt;At least 32 characters. When set, every request needs &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. Needs &lt;code&gt;METRICS_ADDR&lt;/code&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;backd serve&lt;/code&gt;, &lt;code&gt;backd worker&lt;/code&gt;, &lt;code&gt;backd executor&lt;/code&gt; and &lt;code&gt;backd egress&lt;/code&gt; each serve their own &lt;code&gt;/metrics&lt;/code&gt; (the executor and egress read the same two variables); a process that runs both (&lt;code&gt;serve --with-worker&lt;/code&gt;) serves one. The listener answers &lt;code&gt;GET /metrics&lt;/code&gt; and nothing else (a request without the token gets &lt;code&gt;401&lt;/code&gt; whatever the path), serves at most four scrapes at once, and is a separate server: a slow or flooding scraper never slows the API. If the port can&amp;rsquo;t be bound, the process refuses to start, so a mistake shows at once.&lt;/p&gt;</description></item><item><title>Backup and restore</title><link>https://fernandezvara.github.io/backd/docs/operations/backup/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/operations/backup/</guid><description>&lt;p&gt;&lt;code&gt;backd&lt;/code&gt; keeps all state in MongoDB. The config directory describes the structure (collections, validators, indexes, rules), and provisioning can rebuild it at any time, but not the data. Back up MongoDB, and keep the config directory in version control.&lt;/p&gt;
&lt;h2 id="what-a-backup-must-include"&gt;What a backup must include&lt;a class="hd-anchor" href="#what-a-backup-must-include" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Database&lt;/th&gt;
 &lt;th&gt;Holds&lt;/th&gt;
 &lt;th&gt;Rebuilt from config?&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;&amp;lt;realm&amp;gt;__&amp;lt;database&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;your documents&lt;/td&gt;
 &lt;td&gt;no&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;&amp;lt;realm&amp;gt;___system&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the realm&amp;rsquo;s users (emails, roles, disabled flag), password hashes, sessions, API keys, invitations, failed-login counters, the &lt;a href="../../auth/audit/"&gt;audit trail&lt;/a&gt;, encrypted &lt;a href="../../functions/secrets/"&gt;function secrets&lt;/a&gt;, &lt;a href="../../functions/logs/"&gt;invocation history&lt;/a&gt;, &lt;a href="../../functions/jobs/"&gt;async jobs&lt;/a&gt; (queued, running or recently done) and &lt;a href="../../functions/calling/#idempotency"&gt;idempotency claims&lt;/a&gt; (see &lt;a href="../../operations/#realm-system-databases"&gt;system databases&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;no, except role seeds&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;What the configuration can and can&amp;rsquo;t rebuild:&lt;/p&gt;</description></item><item><title>Hardening checklist</title><link>https://fernandezvara.github.io/backd/docs/operations/checklist/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/operations/checklist/</guid><description>&lt;p&gt;Go through this page before any realm faces the internet, and again after changing the configuration or the deployment. The &lt;a href="../production/"&gt;production reference deployment&lt;/a&gt; implements the deployment items, and its test checks many of them.&lt;/p&gt;
&lt;h2 id="realms"&gt;Realms&lt;a class="hd-anchor" href="#realms" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;No realm has &lt;code&gt;auth: disabled&lt;/code&gt;.&lt;/strong&gt; Such a realm is open to anyone who can reach &lt;code&gt;backd&lt;/code&gt;. &lt;code&gt;backd&lt;/code&gt; logs a &lt;code&gt;WARN&lt;/code&gt; line for each one at startup.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The sample realms aren&amp;rsquo;t deployed as they are.&lt;/strong&gt; &lt;code&gt;blog&lt;/code&gt;, &lt;code&gt;shop&lt;/code&gt; and &lt;code&gt;expenses&lt;/code&gt; in &lt;code&gt;examples/config&lt;/code&gt;, and the realms &lt;code&gt;backd template realm --sample&lt;/code&gt; creates, are examples: &lt;code&gt;shop&lt;/code&gt; has &lt;code&gt;auth: disabled&lt;/code&gt;, &lt;code&gt;blog&lt;/code&gt; and &lt;code&gt;expenses&lt;/code&gt; let anyone sign up, and &lt;code&gt;expenses&lt;/code&gt; has &lt;a href="../../examples/expenses/#known-holes"&gt;known holes&lt;/a&gt;. Start from them, then review every setting.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Each realm&amp;rsquo;s &lt;code&gt;signup&lt;/code&gt; mode is the intended one.&lt;/strong&gt; &lt;code&gt;open&lt;/code&gt; lets anyone create an account (and needs the sign-up rate limit below), &lt;code&gt;invite&lt;/code&gt; needs an &lt;a href="../../auth/admin/#invitations"&gt;invitation&lt;/a&gt;, &lt;code&gt;closed&lt;/code&gt; leaves it to operators.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Every collection that users or anonymous callers need has a reviewed &lt;code&gt;rules.yaml&lt;/code&gt;, with a &lt;code&gt;rules.test.yaml&lt;/code&gt;&lt;/strong&gt; that &lt;code&gt;backd rules test --strict&lt;/code&gt; runs in CI: see &lt;a href="../../auth/rules/#testing-rules"&gt;Testing rules&lt;/a&gt;. A missing rule denies; a too-broad one (&lt;code&gt;read: &amp;quot;true&amp;quot;&lt;/code&gt;) publishes a collection. See &lt;a href="../../auth/rules/"&gt;access rules&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;&lt;code&gt;cors.origins&lt;/code&gt; lists exactly your web apps&amp;rsquo; origins&lt;/strong&gt;, and no &lt;code&gt;*&lt;/code&gt; for realms with users. Browser apps can keep sessions in an HttpOnly &lt;a href="../../auth/sessions/#session-cookies"&gt;cookie&lt;/a&gt; (&lt;code&gt;sessions.cookie&lt;/code&gt;), which needs this list.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Someone can administer each realm:&lt;/strong&gt; an &lt;a href="../../configuration/realm/#roles"&gt;admin role&lt;/a&gt; (&lt;code&gt;admin: true&lt;/code&gt;) held by the right people, the first created with &lt;a href="../../auth/cli/#the-first-administrator"&gt;&lt;code&gt;backd bootstrap&lt;/code&gt;&lt;/a&gt;. Startup warns when nobody holds an admin role.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Administrators hold only what they use:&lt;/strong&gt; besides one or two people with &lt;code&gt;admin: true&lt;/code&gt;, roles for the rest list their &lt;a href="../../auth/admin/#admin-rights"&gt;areas&lt;/a&gt; (&lt;code&gt;admin: [users, invitations]&lt;/code&gt;, &lt;code&gt;admin: [audit]&lt;/code&gt;), so the person who invites teammates doesn&amp;rsquo;t hold the secrets and the API keys.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;If you run &lt;a href="../../functions/"&gt;functions&lt;/a&gt;:&lt;/strong&gt; the executor and &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s internal listener (&lt;code&gt;BACKD_INTERNAL_ADDR&lt;/code&gt;) are reachable only from each other, never published; &lt;code&gt;BACKD_EXECUTOR_TOKEN&lt;/code&gt;, &lt;code&gt;BACKD_CALLBACK_KEY&lt;/code&gt;, &lt;code&gt;BACKD_EGRESS_KEY&lt;/code&gt; and, if any function declares secrets, &lt;code&gt;BACKD_SECRETS_KEY&lt;/code&gt; are random, 32+ characters, kept as secrets (never in &lt;code&gt;CONFIG_DIR&lt;/code&gt;) — the &lt;a href="../production/#functions-executor-egress-and-worker"&gt;production reference&lt;/a&gt;&amp;rsquo;s &lt;code&gt;setup.sh&lt;/code&gt; generates all four; &lt;code&gt;admin: true&lt;/code&gt; functions and &lt;code&gt;network&lt;/code&gt; lists are reviewed.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The executor can reach only &lt;code&gt;backd egress&lt;/code&gt; and &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s internal listener — nothing else, including MongoDB, is on its network.&lt;/strong&gt; This is &lt;a href="../../functions/network/#egress-the-network-allowlist-completed"&gt;layer 3 of the egress design&lt;/a&gt;: without it, a function can reach anything its container&amp;rsquo;s network reaches over raw TCP, bypassing both the per-function allowlist and the egress proxy. The &lt;a href="../production/#functions-executor-egress-and-worker"&gt;production reference&lt;/a&gt; wires this up (its own test proves it from a real function call); if you assemble your own deployment instead, &lt;code&gt;docker/egress-test/&lt;/code&gt; in the repository proves the isolation and is the reference to reproduce.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Every function callable anonymously has a &lt;a href="../../functions/calling/#rate-limits"&gt;&lt;code&gt;rate_limit&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt; Startup warns, once, for any that don&amp;rsquo;t — a public function with no per-caller limit can be abused as a relay (e.g. one that sends email).&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Every &lt;a href="../../functions/webhooks/"&gt;&lt;code&gt;webhook&lt;/code&gt;&lt;/a&gt; function verifies its sender&amp;rsquo;s signature itself&lt;/strong&gt;, before trusting the body. &lt;code&gt;backd&lt;/code&gt; only enforces that its &lt;code&gt;invoke&lt;/code&gt; rule allows anonymous callers (it has to — a webhook sender has no session or key); it can&amp;rsquo;t tell a genuine request from a forged one.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;&lt;code&gt;BACKD_SECRETS_KEY&lt;/code&gt; (the master key) is backed up separately from database backups&lt;/strong&gt;, never alongside them: losing it loses every stored function secret, and storing it with a backup defeats that backup&amp;rsquo;s own encryption. See &lt;a href="../backup/#what-a-backup-must-include"&gt;Backup and restore&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;&lt;code&gt;functions.log_retention&lt;/code&gt; and &lt;code&gt;functions.job_retention&lt;/code&gt;&lt;/strong&gt; (&lt;a href="../../functions/logs/"&gt;function logs&lt;/a&gt;, &lt;a href="../../functions/jobs/"&gt;async jobs&lt;/a&gt;) match your privacy policy: both keep a function&amp;rsquo;s own console output, and a job additionally keeps its input and output, in the realm&amp;rsquo;s system database until they expire.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Someone reviews the &lt;a href="../../auth/audit/"&gt;audit trail&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;backd audit --realm &amp;lt;realm&amp;gt; --since 7d&lt;/code&gt;, or the &lt;code&gt;&amp;quot;msg&amp;quot;:&amp;quot;audit&amp;quot;&lt;/code&gt; log lines in your log pipeline), and &lt;code&gt;audit.retention&lt;/code&gt; matches your privacy policy.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Operators&amp;rsquo; machines protect the CLI&amp;rsquo;s sessions&lt;/strong&gt; (&lt;code&gt;~/.config/backd/credentials&lt;/code&gt;): log out with &lt;code&gt;backd logout&lt;/code&gt; on shared machines, and prefer short session lifetimes for realms with administrators.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="credentials"&gt;Credentials&lt;a class="hd-anchor" href="#credentials" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Every API key has an expiry&lt;/strong&gt; (&lt;code&gt;backd apikey create … --expires 90d&lt;/code&gt;), is used by one service, lives in a secret store, and is rotated before it expires. See the &lt;a href="../../auth/api-keys/#expiry-and-rotation-policy"&gt;expiry and rotation policy&lt;/a&gt;. Startup warns about keys without an expiry.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;API keys never reach browsers or mobile apps.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Internet-facing instances serve no admin API&lt;/strong&gt; when an internal instance does: &lt;code&gt;BACKD_ADMIN_API=false&lt;/code&gt; on the public ones (the &lt;a href="../production/#admin-api"&gt;production reference&lt;/a&gt; runs the split), and the CLI&amp;rsquo;s &lt;code&gt;BACKD_URL&lt;/code&gt; points at the internal instance.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The &lt;a href="../../auth/admin-ui/"&gt;admin UI&lt;/a&gt;, if used, is served only by the internal instance&lt;/strong&gt; (&lt;code&gt;BACKD_ADMIN_UI=true&lt;/code&gt; there, nothing on the public one), on its own origin, over TLS, with the proxy passing its &lt;code&gt;Content-Security-Policy&lt;/code&gt; and other headers through unchanged. &lt;code&gt;BACKD_ADMIN_UI_IDLE&lt;/code&gt; is short and the realm&amp;rsquo;s &lt;code&gt;sessions.admin_idle_timeout&lt;/code&gt; and &lt;code&gt;admin_max_lifetime&lt;/code&gt; are shorter still: the page&amp;rsquo;s timer is a convenience, the server&amp;rsquo;s is the limit. See its &lt;a href="../../auth/admin-ui/#security-review"&gt;security review&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Developers who only look hold &lt;code&gt;admin: read&lt;/code&gt;,&lt;/strong&gt; not &lt;code&gt;admin: true&lt;/code&gt;, and &lt;code&gt;admin.read_access.users&lt;/code&gt; / &lt;code&gt;.data&lt;/code&gt; stay &lt;code&gt;false&lt;/code&gt; unless they need personal data. See &lt;a href="../../auth/admin/#the-read-only-level"&gt;The read-only level&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Admin sessions are short:&lt;/strong&gt; &lt;code&gt;sessions.admin_idle_timeout&lt;/code&gt; and &lt;code&gt;sessions.admin_max_lifetime&lt;/code&gt; in each realm&amp;rsquo;s &lt;code&gt;realm.yaml&lt;/code&gt; (for example &lt;code&gt;1h&lt;/code&gt; and &lt;code&gt;12h&lt;/code&gt;), so a stolen administrator&amp;rsquo;s session is worth little. See &lt;a href="../../auth/sessions/#sessions-and-tokens"&gt;Sessions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Keys are scoped to what their service does&lt;/strong&gt; (&lt;code&gt;--scopes read:main/posts,call:main/contact&lt;/code&gt;): a leaked key then exposes that and nothing more. See &lt;a href="../../auth/api-keys/#scopes"&gt;Scopes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Services use &lt;code&gt;data&lt;/code&gt; keys&lt;/strong&gt;; &lt;code&gt;admin&lt;/code&gt; keys (&lt;code&gt;--role admin&lt;/code&gt;) exist only for management tooling.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Secrets&lt;/strong&gt; (MongoDB passwords, TLS keys, the CA key, &lt;code&gt;.env&lt;/code&gt;) are outside version control and readable only by the services that need them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-edge"&gt;The edge&lt;a class="hd-anchor" href="#the-edge" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;TLS everywhere:&lt;/strong&gt; the edge terminates TLS with a trusted certificate that renews automatically, and plain HTTP only redirects. MongoDB connections use TLS too (below).&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Rate limits:&lt;/strong&gt; sign-up, login and the account flows (emailed links, password and address changes) have a strict per-address limit, and the rest of &lt;code&gt;/v1/&lt;/code&gt; a general one. Requests over a limit get &lt;code&gt;429&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Body size:&lt;/strong&gt; the edge caps request bodies at &lt;code&gt;MAX_BODY_BYTES&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Client addresses:&lt;/strong&gt; the edge overwrites &lt;code&gt;X-Forwarded-For&lt;/code&gt;, and &lt;code&gt;TRUSTED_PROXIES&lt;/code&gt; names exactly the edge&amp;rsquo;s addresses, never whole private ranges. &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s access log shows real client addresses.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Only the edge is reachable:&lt;/strong&gt; &lt;code&gt;backd&lt;/code&gt; isn&amp;rsquo;t published directly, and &lt;code&gt;/healthz&lt;/code&gt; and &lt;code&gt;/readyz&lt;/code&gt; aren&amp;rsquo;t public.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The admin API&lt;/strong&gt; is only reachable from your operators&amp;rsquo; network: &lt;code&gt;admin.allowed_networks&lt;/code&gt; in each &lt;code&gt;realm.yaml&lt;/code&gt; (and &lt;code&gt;admin_networks&lt;/code&gt; per admin where useful), plus &lt;code&gt;ADMIN_ALLOW_FROM&lt;/code&gt; at the edge in the reference.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The &lt;a href="../metrics/"&gt;metrics&lt;/a&gt; port is private&lt;/strong&gt; (&lt;code&gt;METRICS_ADDR&lt;/code&gt; is never published or proxied, and &lt;code&gt;METRICS_TOKEN&lt;/code&gt; is set if anything but Prometheus can reach it).&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;API keys of services with fixed addresses are pinned&lt;/strong&gt; to them with &lt;code&gt;--networks&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="mongodb"&gt;MongoDB&lt;a class="hd-anchor" href="#mongodb" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;MongoDB requires TLS and authentication&lt;/strong&gt;, and isn&amp;rsquo;t reachable from the internet.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;&lt;code&gt;backd&lt;/code&gt; runs with &lt;code&gt;PROVISION_MODE=verify&lt;/code&gt;&lt;/strong&gt; as a user limited to the configured collections; admin credentials are only available to the provisioning step. See &lt;a href="../production/#least-privilege-provisioning"&gt;least-privilege provisioning&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Encrypted backups&lt;/strong&gt; of every data and &lt;code&gt;&amp;lt;realm&amp;gt;___system&lt;/code&gt; database run on a schedule, are copied off the server, and a restore has been practised. The decryption key isn&amp;rsquo;t on the server. See &lt;a href="../backup/"&gt;Backup and restore&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="operations"&gt;Operations&lt;a class="hd-anchor" href="#operations" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;&lt;code&gt;LOG_LEVEL&lt;/code&gt; is &lt;code&gt;info&lt;/code&gt;&lt;/strong&gt; (the default). &lt;code&gt;debug&lt;/code&gt; logs every access denial with its reason, which is useful while writing rules but noisy and revealing in production.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;Logs are collected and kept&lt;/strong&gt;, and startup &lt;code&gt;WARN&lt;/code&gt; lines are reviewed: realms with &lt;code&gt;auth: disabled&lt;/code&gt;, realms without rules, API keys without an expiry or about to expire, anonymous-callable functions with no &lt;code&gt;rate_limit&lt;/code&gt;, and (if you run functions) an executor that isn&amp;rsquo;t configured or doesn&amp;rsquo;t answer.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;The container runs hardened:&lt;/strong&gt; read-only filesystem, no Linux capabilities, CPU and memory limits (they also bound &lt;a href="../../auth/users/#password-storage"&gt;password hashing&lt;/a&gt;), and a stop grace period longer than &lt;code&gt;SHUTDOWN_TIMEOUT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;strong&gt;You know how to report and receive security fixes:&lt;/strong&gt; see &lt;a href="https://github.com/fernandezvara/backd/blob/main/SECURITY.md"&gt;&lt;code&gt;SECURITY.md&lt;/code&gt;&lt;/a&gt;. Only the latest release gets them.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Command-line administration</title><link>https://fernandezvara.github.io/backd/docs/auth/cli/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/cli/</guid><description>&lt;p&gt;The &lt;code&gt;backd&lt;/code&gt; binary is also the command-line client for administering a realm. &lt;code&gt;backd user&lt;/code&gt; and &lt;code&gt;backd apikey&lt;/code&gt; don&amp;rsquo;t touch MongoDB: they call the realm&amp;rsquo;s &lt;a href="../admin/"&gt;admin API&lt;/a&gt; on a running &lt;code&gt;backd&lt;/code&gt;, as an administrator. They never need database credentials, and every change they make goes through the same checks as any other admin request, including &lt;a href="../../configuration/realm/#network-restrictions"&gt;network restrictions&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Two commands write to MongoDB directly instead: &lt;code&gt;backd bootstrap&lt;/code&gt;, which creates a realm&amp;rsquo;s first administrator, and &lt;code&gt;backd secret rotate-key&lt;/code&gt;, which re-encrypts every realm&amp;rsquo;s &lt;a href="../../functions/secrets/"&gt;secrets&lt;/a&gt; under a new master key (see &lt;a href="../admin/#secrets"&gt;Secrets&lt;/a&gt;).&lt;/p&gt;</description></item><item><title>Users and passwords</title><link>https://fernandezvara.github.io/backd/docs/auth/users/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/users/</guid><description>&lt;p&gt;The &lt;code&gt;backd user&lt;/code&gt; commands manage the users of a realm with &lt;code&gt;auth: enabled&lt;/code&gt;. They call the realm&amp;rsquo;s &lt;a href="../admin/"&gt;admin API&lt;/a&gt; on a running &lt;code&gt;backd&lt;/code&gt;, so you first &lt;a href="../cli/#logging-in"&gt;log in&lt;/a&gt; as an administrator (or set &lt;code&gt;BACKD_API_KEY&lt;/code&gt; to an admin API key). The realm&amp;rsquo;s first administrator is created with &lt;a href="../cli/#the-first-administrator"&gt;&lt;code&gt;backd bootstrap&lt;/code&gt;&lt;/a&gt;. Server-side services can call the admin API directly.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backd login --realm blog --url https://api.example.com
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backd user list --realm blog
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every command also takes &lt;code&gt;--url&lt;/code&gt;, to pick the server for that command.&lt;/p&gt;</description></item><item><title>Sign-up, login and sessions</title><link>https://fernandezvara.github.io/backd/docs/auth/sessions/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/sessions/</guid><description>&lt;p&gt;What a signed-in user may do with data is decided by each collection&amp;rsquo;s &lt;a href="../rules/"&gt;access rules&lt;/a&gt;. Server-side services use &lt;a href="../api-keys/"&gt;API keys&lt;/a&gt; instead.&lt;/p&gt;
&lt;p&gt;Every realm with &lt;code&gt;auth: enabled&lt;/code&gt; has its authentication endpoints under &lt;code&gt;/v1/{realm}/_auth&lt;/code&gt;. A realm that doesn&amp;rsquo;t exist or has &lt;code&gt;auth: disabled&lt;/code&gt; answers &lt;code&gt;404 not_found&lt;/code&gt; on all of them.&lt;/p&gt;
&lt;h2 id="sessions-and-tokens"&gt;Sessions and tokens&lt;a class="hd-anchor" href="#sessions-and-tokens" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Signing up or logging in returns a &lt;strong&gt;session token&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bds_mxamh1_afbN3BMzfXqDWhxYpXKF58FhWQ4r9eaLc9ew&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;token_type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bearer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;session_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dars2ql90v6000bbohr0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;expires_at&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026-10-26T12:58:18.988Z&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dars2ql90v600434mf0g&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ada@example.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;email_verified&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;roles&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;locale&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;created_at&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026-09-26T12:58:18.838Z&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Send it on every request that needs a signed-in user:&lt;/p&gt;</description></item><item><title>API keys</title><link>https://fernandezvara.github.io/backd/docs/auth/api-keys/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/api-keys/</guid><description>&lt;p&gt;API keys let server-side services (a backend, a worker, a script) call a realm&amp;rsquo;s data routes. A key belongs to one realm and gives &lt;strong&gt;full access to all of that realm&amp;rsquo;s data&lt;/strong&gt;, in every database.
&lt;aside class="hd-hint hd-hint--danger"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;circle cx="12" cy="12" r="9"/&gt;&lt;path d="m9 9 6 6M15 9l-6 6"/&gt;&lt;/svg&gt;
Danger&lt;/p&gt;
 A key is a password with no user behind it: it bypasses every &lt;a href="../rules/"&gt;access rule&lt;/a&gt;. &lt;strong&gt;Never ship one to a browser or a mobile app&lt;/strong&gt;, where every visitor can read it. Use it only from servers you control, keep it in a secret store, and give it an expiry.
&lt;/aside&gt;
&lt;/p&gt;</description></item><item><title>Access rules (rules:)</title><link>https://fernandezvara.github.io/backd/docs/auth/rules/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/rules/</guid><description>&lt;p&gt;Each collection of a realm with &lt;code&gt;auth: enabled&lt;/code&gt; can have a &lt;code&gt;rules:&lt;/code&gt; section in its &lt;code&gt;collection.yaml&lt;/code&gt;, next to its &lt;code&gt;schema.json&lt;/code&gt;. Rules decide, per operation, whether a caller may act on a &lt;strong&gt;whole document&lt;/strong&gt;. There are no per-field permissions: data that needs different visibility belongs in a separate collection with its own rules.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;$CONFIG_DIR/&amp;lt;realm&amp;gt;/&amp;lt;database&amp;gt;/&amp;lt;collection&amp;gt;/
 schema.json
 indexes.json # optional
 collection.yaml # optional — its `rules:` section is the access rules
 rules.test.yaml # optional — tests for the rules
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The same file holds the collection&amp;rsquo;s other policy: &lt;a href="../../files/file-fields/"&gt;&lt;code&gt;files:&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;soft_delete:&lt;/code&gt; and &lt;code&gt;on_owner_delete:&lt;/code&gt; (see &lt;a href="../../configuration/config-dir/#collectionyaml"&gt;the collection file&lt;/a&gt;). This page is about the &lt;code&gt;rules:&lt;/code&gt; key.&lt;/p&gt;</description></item><item><title>Admin API</title><link>https://fernandezvara.github.io/backd/docs/auth/admin/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/admin/</guid><description>&lt;p&gt;The admin API manages the users and API keys of a realm. &lt;a href="../users/"&gt;&lt;code&gt;backd user&lt;/code&gt;&lt;/a&gt; and &lt;a href="../api-keys/"&gt;&lt;code&gt;backd apikey&lt;/code&gt;&lt;/a&gt; are command-line clients of it (see &lt;a href="../cli/"&gt;Command-line administration&lt;/a&gt;), and server-side services can call it directly. It lives under &lt;code&gt;/v1/{realm}/_admin&lt;/code&gt; in every realm with &lt;code&gt;auth: enabled&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Every request needs one of the realm&amp;rsquo;s &lt;a href="../api-keys/"&gt;API keys&lt;/a&gt; with the &lt;code&gt;admin&lt;/code&gt; role, acting as itself, or the session of a user holding one of the realm&amp;rsquo;s &lt;a href="../../configuration/realm/#roles"&gt;admin roles&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Credential&lt;/th&gt;
 &lt;th&gt;Answer&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;API key with the &lt;code&gt;admin&lt;/code&gt; role&lt;/td&gt;
 &lt;td&gt;Allowed&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Session of a user holding an admin role (&lt;code&gt;admin: true&lt;/code&gt; in &lt;code&gt;realm.yaml&lt;/code&gt;)&lt;/td&gt;
 &lt;td&gt;Allowed&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Session of a user whose admin role opens only some &lt;a href="#admin-rights"&gt;areas&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;Allowed for those areas; &lt;code&gt;403 forbidden&lt;/code&gt; naming the missing one for the rest&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;API key with the &lt;code&gt;data&lt;/code&gt; role&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;403 forbidden&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;None, or an invalid key&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;401 unauthenticated&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Session of a user without an admin role&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;403 forbidden&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;X-Backd-On-Behalf-Of&lt;/code&gt; with any credential&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;400 invalid_header&lt;/code&gt;: admin calls never act as another user&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class="hd-hint hd-hint--danger"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;circle cx="12" cy="12" r="9"/&gt;&lt;path d="m9 9 6 6M15 9l-6 6"/&gt;&lt;/svg&gt;
Danger&lt;/p&gt;</description></item><item><title>Quickstart</title><link>https://fernandezvara.github.io/backd/docs/functions/quickstart/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/quickstart/</guid><description>&lt;p&gt;You will create a config repository with a working sample function, run it locally, call it three ways, write your own, see what it did, and test it. You need Docker (or Podman) and the &lt;code&gt;backd&lt;/code&gt; binary; Deno is optional, because the build can run in Deno&amp;rsquo;s own image.&lt;/p&gt;
&lt;h2 id="1-create-a-project"&gt;1. Create a project&lt;a class="hd-anchor" href="#1-create-a-project" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backd template project --dir ./shop --realm shop
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; shop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This writes a complete repository: the &lt;code&gt;shop&lt;/code&gt; realm with a sample database, a sample function with a test, Dockerfiles, a &lt;code&gt;compose.yaml&lt;/code&gt; for local development (MongoDB, &lt;code&gt;backd&lt;/code&gt;, the functions executor and the egress proxy) and a CI workflow. Everything you need is in the generated &lt;code&gt;README.md&lt;/code&gt;; the parts below follow it.&lt;/p&gt;</description></item><item><title>Admin UI</title><link>https://fernandezvara.github.io/backd/docs/auth/admin-ui/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/admin-ui/</guid><description>&lt;p&gt;The admin UI is a web interface for realm administrators and developers. It is a client of the &lt;a href="../admin/"&gt;Admin API&lt;/a&gt; and nothing more: it can do nothing the CLI can&amp;rsquo;t, and every call it makes is authenticated, network-checked and audited by the server like any other. This page is the start of its guide; each area of the interface (users, keys, secrets, audit, functions, data, configuration) is added to it as the area ships.&lt;/p&gt;</description></item><item><title>Writing a function</title><link>https://fernandezvara.github.io/backd/docs/functions/writing/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/writing/</guid><description>&lt;h2 id="layout"&gt;Layout&lt;a class="hd-anchor" href="#layout" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Each database may have one &lt;code&gt;_functions&lt;/code&gt; directory: a Deno project for that application&amp;rsquo;s functions.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;realm&amp;gt;/&amp;lt;database&amp;gt;/
 posts/ collections, as usual
 _functions/
 deno.json the project&amp;#39;s Deno config (imports), optional
 deno.lock pinned dependency versions, written by Deno
 lib/ code shared by the functions (not a function)
 stats/ one folder per function, named like collections
 function.yaml how the function runs (all keys optional)
 index.ts the code: index.ts or index.js
 input.schema.json optional JSON Schema of the input
 output.schema.json optional JSON Schema of the output
 .build/ bundles, written by `backd functions build`
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Function names follow the same rules as collection names; &lt;code&gt;lib&lt;/code&gt; is reserved for shared code.&lt;/li&gt;
&lt;li&gt;The module&amp;rsquo;s &lt;strong&gt;default export&lt;/strong&gt; is the function: it receives the call&amp;rsquo;s context and returns the output (any JSON value).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;input&lt;/span&gt;: &lt;span class="kt"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;email_verified&lt;/span&gt;: &lt;span class="kt"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;: &lt;span class="kt"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hello&lt;/span&gt;: &lt;span class="kt"&gt;ctx.user?.email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;anonymous&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create a starter with:&lt;/p&gt;</description></item><item><title>Calling functions</title><link>https://fernandezvara.github.io/backd/docs/functions/calling/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/calling/</guid><description>&lt;h2 id="calling-a-function"&gt;Calling a function&lt;a class="hd-anchor" href="#calling-a-function" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -X POST https://api.example.com/v1/shop/orders/_func/checkout &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -H &lt;span class="s2"&gt;&amp;#34;Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;cart&amp;#34;: &amp;#34;c1&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# → 200 {&amp;#34;order&amp;#34;: &amp;#34;o7&amp;#34;, &amp;#34;total&amp;#34;: 1250}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;The body is the function&amp;rsquo;s input (&lt;code&gt;ctx.input&lt;/code&gt;), any JSON value; an empty body is &lt;code&gt;null&lt;/code&gt;. The answer is its output. &lt;strong&gt;&lt;code&gt;webhook&lt;/code&gt; functions are different:&lt;/strong&gt; see &lt;a href="../webhooks/"&gt;Webhooks&lt;/a&gt; — the raw body reaches &lt;code&gt;ctx.request.body&lt;/code&gt; unparsed, &lt;code&gt;Content-Type&lt;/code&gt; isn&amp;rsquo;t required to be JSON, and the function&amp;rsquo;s own response is returned as-is.&lt;/li&gt;
&lt;li&gt;In realms with &lt;code&gt;auth: enabled&lt;/code&gt;, &lt;a href="../../auth/api-keys/"&gt;API keys&lt;/a&gt; may always call; other callers need the function&amp;rsquo;s &lt;a href="../reference/#invoke-rules"&gt;&lt;code&gt;invoke&lt;/code&gt; rule&lt;/a&gt; (&lt;code&gt;webhook&lt;/code&gt; always needs one that allows anonymous callers). With &lt;code&gt;X-Backd-On-Behalf-Of&lt;/code&gt;, an API key calls as that user.&lt;/li&gt;
&lt;li&gt;With &lt;code&gt;input.schema.json&lt;/code&gt;, invalid input answers &lt;code&gt;400 validation_error&lt;/code&gt;; with &lt;code&gt;output.schema.json&lt;/code&gt;, an output that doesn&amp;rsquo;t match answers &lt;code&gt;500 invalid_output&lt;/code&gt; (and is logged). Neither applies to &lt;code&gt;webhook&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;Idempotency-Key&lt;/code&gt; header reaches the function as &lt;code&gt;ctx.idempotencyKey&lt;/code&gt;, and also deduplicates the call itself (see &lt;a href="#idempotency"&gt;Idempotency&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sync&lt;/code&gt; functions run immediately and answer with their output, as above. &lt;code&gt;async&lt;/code&gt; functions (&lt;a href="../jobs/"&gt;below&lt;/a&gt;) answer &lt;code&gt;202&lt;/code&gt; with a job to poll. &lt;code&gt;webhook&lt;/code&gt; functions (&lt;a href="../webhooks/"&gt;below&lt;/a&gt;) set their own response.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Answer&lt;/th&gt;
 &lt;th&gt;When&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A &lt;code&gt;sync&lt;/code&gt; function returned; the body is its output&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;202&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An &lt;code&gt;async&lt;/code&gt; function was queued; poll &lt;code&gt;Location&lt;/code&gt; for its result (see &lt;a href="../jobs/"&gt;Async jobs&lt;/a&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;em&gt;(the function&amp;rsquo;s own choice)&lt;/em&gt;&lt;/td&gt;
 &lt;td&gt;A &lt;code&gt;webhook&lt;/code&gt; function&amp;rsquo;s own response (see &lt;a href="../webhooks/"&gt;Webhooks&lt;/a&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;4xx&lt;/code&gt; with the function&amp;rsquo;s code&lt;/td&gt;
 &lt;td&gt;The function threw &lt;code&gt;ctx.error(status, code, message)&lt;/code&gt; (for &lt;code&gt;async&lt;/code&gt;, this lands in the job&amp;rsquo;s &lt;code&gt;result&lt;/code&gt; instead; &lt;code&gt;webhook&lt;/code&gt; functions answer errors through their own response, not &lt;code&gt;ctx.error&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;400 validation_error&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The input doesn&amp;rsquo;t match &lt;code&gt;input.schema.json&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;400 idempotency_key_required&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;function.yaml&lt;/code&gt; has &lt;code&gt;idempotency: required&lt;/code&gt; and the call has no &lt;code&gt;Idempotency-Key&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;401&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The &lt;code&gt;invoke&lt;/code&gt; rule doesn&amp;rsquo;t allow the caller&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;404 not_found&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;No such function&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;409 request_in_progress&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A call with this &lt;code&gt;Idempotency-Key&lt;/code&gt; is still running (see &lt;a href="#idempotency"&gt;Idempotency&lt;/a&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;422 idempotency_key_reused&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;This &lt;code&gt;Idempotency-Key&lt;/code&gt; was already used with different input&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;429 too_many_requests&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A &lt;code&gt;sync&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt; function, or its realm, is at its &lt;a href="#concurrency-limits"&gt;concurrency limit&lt;/a&gt; on this instance (&lt;code&gt;Retry-After: 1&lt;/code&gt;); or the caller is over the function&amp;rsquo;s &lt;a href="#rate-limits"&gt;rate limit&lt;/a&gt; (&lt;code&gt;Retry-After&lt;/code&gt; set to when it resets)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;500 function_failed&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The function threw another error, crashed, or went over its memory or CPU limit. The stack trace is in &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s log only&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;500 invalid_output&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The output is larger than &lt;code&gt;max_output&lt;/code&gt;, or doesn&amp;rsquo;t match &lt;code&gt;output.schema.json&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;500 secret_missing&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The function declares a secret that isn&amp;rsquo;t set for its database or realm&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;503 unavailable&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The executor isn&amp;rsquo;t configured, doesn&amp;rsquo;t answer, or runs its maximum of functions (&lt;code&gt;Retry-After: 1&lt;/code&gt;); or a &lt;code&gt;webhook&lt;/code&gt; function is at its &lt;a href="#concurrency-limits"&gt;concurrency limit&lt;/a&gt; (&lt;code&gt;Retry-After: 1&lt;/code&gt; — &lt;code&gt;503&lt;/code&gt;, not &lt;code&gt;429&lt;/code&gt;, since webhook senders retry on 5xx)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;504 function_timeout&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The function didn&amp;rsquo;t finish within its &lt;code&gt;timeout&lt;/code&gt;; it was stopped&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="from-the-javascript-client-and-the-command-line"&gt;From the JavaScript client and the command line&lt;a class="hd-anchor" href="#from-the-javascript-client-and-the-command-line" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;The same call, three ways:&lt;/p&gt;</description></item><item><title>function.yaml reference</title><link>https://fernandezvara.github.io/backd/docs/functions/reference/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/reference/</guid><description>&lt;h2 id="functionyaml"&gt;function.yaml&lt;a class="hd-anchor" href="#functionyaml" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Every key is optional. &lt;code&gt;backd&lt;/code&gt; reads the file strictly: an unknown key or an invalid value stops startup, naming the file.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Key&lt;/th&gt;
 &lt;th&gt;Default&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;mode&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;sync&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;sync&lt;/code&gt;: the caller waits for the output. &lt;code&gt;async&lt;/code&gt;: the caller gets a &lt;a href="../jobs/"&gt;job&lt;/a&gt; to check later. &lt;code&gt;webhook&lt;/code&gt;: for callbacks from other services (see &lt;a href="../webhooks/"&gt;Webhooks&lt;/a&gt;) — needs &lt;code&gt;invoke&lt;/code&gt; set to a rule that allows anonymous callers&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;10s&lt;/code&gt; (&lt;code&gt;sync&lt;/code&gt;, &lt;code&gt;webhook&lt;/code&gt;), &lt;code&gt;15m&lt;/code&gt; (&lt;code&gt;async&lt;/code&gt;)&lt;/td&gt;
 &lt;td&gt;The function is stopped after this long. At least &lt;code&gt;1s&lt;/code&gt;; at most &lt;code&gt;60s&lt;/code&gt; for &lt;code&gt;sync&lt;/code&gt; and &lt;code&gt;webhook&lt;/code&gt;, &lt;code&gt;24h&lt;/code&gt; for &lt;code&gt;async&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;memory&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;128MB&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Memory for the function&amp;rsquo;s JavaScript heap, from &lt;code&gt;32MiB&lt;/code&gt; to &lt;code&gt;4GiB&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_output&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;1MiB&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The largest output accepted, from &lt;code&gt;1KiB&lt;/code&gt; to &lt;code&gt;64MiB&lt;/code&gt; (&lt;code&gt;15MiB&lt;/code&gt; for &lt;code&gt;async&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;concurrency&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;How many calls of the function may run at once &lt;strong&gt;on each &lt;code&gt;backd&lt;/code&gt; instance&lt;/strong&gt; (1–1000). With several instances, up to that many times this value run at once&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;rate_limit&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Calls allowed per caller, counted across instances: &lt;code&gt;{per: user | ip, limit: &amp;lt;n&amp;gt;, window: &amp;lt;duration&amp;gt;}&lt;/code&gt;, window from &lt;code&gt;1s&lt;/code&gt; to &lt;code&gt;24h&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;idempotency&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;optional&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Whether calls must send an &lt;code&gt;Idempotency-Key&lt;/code&gt; header: &lt;code&gt;optional&lt;/code&gt; or &lt;code&gt;required&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;invoke&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Who may call the function (see below). Without it, only &lt;a href="../../auth/api-keys/"&gt;API keys&lt;/a&gt; may call it&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;true&lt;/code&gt; gives the function full access to the realm&amp;rsquo;s data, whatever the caller may do. Keep it off unless the function needs it&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;true&lt;/code&gt; removes the function&amp;rsquo;s HTTP route: &lt;code&gt;_func/&amp;lt;name&amp;gt;&lt;/code&gt; answers &lt;code&gt;404&lt;/code&gt; to everyone, like a function that doesn&amp;rsquo;t exist. Only &lt;code&gt;backd&lt;/code&gt; and scheduled runs may call it (see &lt;a href="../internal/"&gt;Internal functions&lt;/a&gt;). Can&amp;rsquo;t be combined with &lt;code&gt;invoke&lt;/code&gt; or &lt;code&gt;mode: webhook&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;dev_only&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;true&lt;/code&gt; makes &lt;code&gt;backd&lt;/code&gt; refuse to start with the function unless &lt;code&gt;BACKD_DEV=true&lt;/code&gt;: for development helpers such as the &lt;a href="../email/#developing-without-a-provider"&gt;email capture&lt;/a&gt; function, which must never run in production&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;email&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;true&lt;/code&gt; gives the function &lt;code&gt;ctx.email.send()&lt;/code&gt;: &lt;a href="../email/#sending-email-from-functions"&gt;custom emails&lt;/a&gt; through the realm&amp;rsquo;s templates and delivery function, to any recipient, within the caps of &lt;code&gt;email.limits&lt;/code&gt;. Needs &lt;code&gt;email&lt;/code&gt; in &lt;code&gt;realm.yaml&lt;/code&gt;; read the warning on that page first&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;calls&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;The functions of the same database this one lists as callable (see &lt;a href="../internal/#declaring-which-functions-it-may-call"&gt;Internal functions&lt;/a&gt;); startup rejects unknown names, cycles and chains longer than 4 functions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;retry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Tries a failed &lt;code&gt;async&lt;/code&gt; function again: &lt;code&gt;{attempts, backoff, max_backoff}&lt;/code&gt; (see &lt;a href="../jobs/#retrying-a-failed-job"&gt;Retrying a failed job&lt;/a&gt;). A single attempt unless &lt;code&gt;attempts&lt;/code&gt; is above 1&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;on_complete&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;For an &lt;code&gt;async&lt;/code&gt; function: &lt;code&gt;{function, on?}&lt;/code&gt; queues that async function of the same database when a job of this one ends, with the outcome as its input; &lt;code&gt;on&lt;/code&gt; narrows it to &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;cancelled&lt;/code&gt;. See &lt;a href="../jobs/#be-told-when-it-ends-on_complete"&gt;Be told when it ends&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;schedule&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;A cron expression: the function &lt;a href="../cron/"&gt;runs on a schedule&lt;/a&gt;. Needs &lt;code&gt;mode: async&lt;/code&gt; and at least one running &lt;a href="../running/#running-the-worker"&gt;worker&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;timezone&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;UTC&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;For a scheduled function: the IANA time zone the &lt;a href="../cron/#time-zones"&gt;schedule&lt;/a&gt; is read in, such as &lt;code&gt;Europe/Madrid&lt;/code&gt;. Daylight saving changes are handled (documented there). Needs &lt;code&gt;schedule&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;overlap&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;For a scheduled function: &lt;code&gt;skip&lt;/code&gt; doesn&amp;rsquo;t run a scheduled time while the function&amp;rsquo;s previous job hasn&amp;rsquo;t finished, and records the time as a &lt;a href="../cron/#skipping-a-run-while-the-previous-one-is-going"&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/a&gt; job. Needs &lt;code&gt;schedule&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;secrets&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Secrets the function may read: &lt;code&gt;NAME&lt;/code&gt; (its own database&amp;rsquo;s) or &lt;code&gt;realm.NAME&lt;/code&gt; (the realm&amp;rsquo;s). Names are upper-case letters, digits and &lt;code&gt;_&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;network&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Hosts the function may call: &lt;code&gt;host&lt;/code&gt; or &lt;code&gt;host:port&lt;/code&gt;, lower case, no scheme, path or wildcard. Without a list, a function can only call &lt;code&gt;backd&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Durations are written as in &lt;a href="../../configuration/realm/"&gt;&lt;code&gt;realm.yaml&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;30s&lt;/code&gt;, &lt;code&gt;15m&lt;/code&gt;, &lt;code&gt;1d&lt;/code&gt;). Sizes need a unit: &lt;code&gt;B&lt;/code&gt;, &lt;code&gt;KB&lt;/code&gt;, &lt;code&gt;MB&lt;/code&gt;, &lt;code&gt;GB&lt;/code&gt; (powers of 1000) or &lt;code&gt;KiB&lt;/code&gt;, &lt;code&gt;MiB&lt;/code&gt;, &lt;code&gt;GiB&lt;/code&gt; (powers of 1024).&lt;/p&gt;</description></item><item><title>Async jobs</title><link>https://fernandezvara.github.io/backd/docs/functions/jobs/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/jobs/</guid><description>&lt;p&gt;&lt;code&gt;mode: async&lt;/code&gt; runs a function in the background instead of within the request. The call answers &lt;code&gt;202&lt;/code&gt; at once with a &lt;strong&gt;job&lt;/strong&gt;, and a &lt;a href="../running/#running-the-worker"&gt;worker&lt;/a&gt; runs the function later, on its own schedule and for as long as its &lt;code&gt;timeout&lt;/code&gt; allows.&lt;/p&gt;
&lt;p&gt;Use it when the work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;takes more than a few seconds (a &lt;code&gt;sync&lt;/code&gt; call is capped at 60 s, and a caller shouldn&amp;rsquo;t be made to hold a request open for anything close to that);&lt;/li&gt;
&lt;li&gt;must survive the caller going away (a browser tab closed, a mobile app suspended);&lt;/li&gt;
&lt;li&gt;can be retried safely if something goes wrong;&lt;/li&gt;
&lt;li&gt;fans out (a batch of emails, a report over many documents, a slow third-party call).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use a &lt;code&gt;sync&lt;/code&gt; function when the caller needs the answer to continue and it&amp;rsquo;s quick. Use &lt;a href="../cron/"&gt;cron&lt;/a&gt; when nobody asks for the work: it just has to happen on a schedule.&lt;/p&gt;</description></item><item><title>Scheduled functions</title><link>https://fernandezvara.github.io/backd/docs/functions/cron/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/cron/</guid><description>&lt;p&gt;Add &lt;code&gt;schedule&lt;/code&gt; to an &lt;code&gt;async&lt;/code&gt; function and it runs by itself, with no caller: nightly cleanups, daily digests, hourly syncs, monthly reports.&lt;/p&gt;
&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/workshop/main/_functions/nightly_cleanup/function.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# A scheduled function: `schedule` (cron, UTC) on an async function. There is&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# no caller, so ctx.input is empty, ctx.user is null, and ctx.admin.db (full&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# access, recorded as this function) is always available in a scheduled run.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Needs at least one running worker: `backd worker`, or&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# `backd serve --with-worker`.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# `internal: true`: nobody can call it over HTTP. An administrator can still&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# run it by hand to test it, as its schedule would run it:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# backd functions invoke --function workshop/main/nightly_cleanup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;async&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;0 3 * * *&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# every day at 03:00 UTC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;10m&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p class="hd-example-path"&gt;&lt;code&gt;examples/config/workshop/main/_functions/nightly_cleanup/index.ts&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Internal functions</title><link>https://fernandezvara.github.io/backd/docs/functions/internal/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/internal/</guid><description>&lt;p&gt;Some functions must never be reachable from the internet: a clean-up that deletes data, a helper shared by several public functions, a scheduled job nobody should trigger by hand. An &lt;strong&gt;internal&lt;/strong&gt; function has no HTTP route at all.&lt;/p&gt;
&lt;h2 id="marking-a-function-internal"&gt;Marking a function internal&lt;a class="hd-anchor" href="#marking-a-function-internal" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# _functions/cleanup/function.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;async&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;POST /v1/&amp;lt;realm&amp;gt;/&amp;lt;database&amp;gt;/_func/cleanup&lt;/code&gt; now answers &lt;code&gt;404 not_found&lt;/code&gt;, exactly like a function that doesn&amp;rsquo;t exist, to every caller: anonymous, signed in, API keys of either role. Nothing reaches the executor, and the answer doesn&amp;rsquo;t reveal that the function exists.&lt;/p&gt;</description></item><item><title>Email</title><link>https://fernandezvara.github.io/backd/docs/functions/email/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/email/</guid><description>&lt;p&gt;&lt;code&gt;backd&lt;/code&gt; &lt;strong&gt;never talks to a mail server&lt;/strong&gt;. A realm names one &lt;strong&gt;delivery function&lt;/strong&gt;: an &lt;a href="../internal/"&gt;internal&lt;/a&gt;, &lt;code&gt;async&lt;/code&gt; function that receives a finished message and hands it to whatever provider you chose (Postmark, SES, Resend, Mailgun, SendGrid, …). &lt;code&gt;backd&lt;/code&gt; does everything else: the templates, the tokens and links, the retries, the limits.&lt;/p&gt;
&lt;aside class="hd-hint hd-hint--note"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;circle cx="12" cy="12" r="9"/&gt;&lt;path d="M12 16v-4m0-4h.01"/&gt;&lt;/svg&gt;
Note&lt;/p&gt;
 &lt;code&gt;backd&lt;/code&gt; sends no mail from its own servers or addresses, so &lt;strong&gt;deliverability is your responsibility&lt;/strong&gt;, handled with your provider: a verified sending domain (SPF, DKIM, DMARC), a dedicated sender address, the provider&amp;rsquo;s bounce and complaint handling and suppression lists. Watch your provider&amp;rsquo;s bounce and complaint rates before sending real email, and try the flows with a test mailbox first.
&lt;/aside&gt;

&lt;h2 id="configuration"&gt;Configuration&lt;a class="hd-anchor" href="#configuration" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Email is configured in &lt;a href="../../configuration/realm/"&gt;&lt;code&gt;realm.yaml&lt;/code&gt;&lt;/a&gt;; &lt;code&gt;backd template realm&lt;/code&gt; writes the section commented, with every key explained. A realm without &lt;code&gt;email&lt;/code&gt; sends no email at all.&lt;/p&gt;</description></item><item><title>Webhooks</title><link>https://fernandezvara.github.io/backd/docs/functions/webhooks/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/webhooks/</guid><description>&lt;p&gt;&lt;code&gt;mode: webhook&lt;/code&gt; receives a callback from another service — Stripe, GitHub, and the like — and answers it directly: &lt;code&gt;ctx&lt;/code&gt; carries the raw request instead of parsed input, and the function sets the response&amp;rsquo;s status, headers and body itself instead of returning a JSON value backd interprets.&lt;/p&gt;
&lt;aside class="hd-hint hd-hint--warning"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;path d="M12 3 2 21h20Z"/&gt;&lt;path d="M12 10v4m0 3h.01"/&gt;&lt;/svg&gt;
Warning&lt;/p&gt;
 A webhook function is callable by &lt;strong&gt;anyone on the internet&lt;/strong&gt; (&lt;code&gt;invoke&lt;/code&gt; must allow anonymous callers), so the function itself is the only check. Verify the sender&amp;rsquo;s signature against the raw body before doing anything, and deduplicate by the sender&amp;rsquo;s event id: providers retry.
&lt;/aside&gt;

&lt;p&gt;Payment provider example: &lt;code&gt;payment_webhook&lt;/code&gt; verifies the sender&amp;rsquo;s HMAC signature, deduplicates the provider&amp;rsquo;s retries by event id, and marks the order paid.&lt;/p&gt;</description></item><item><title>Audit trail</title><link>https://fernandezvara.github.io/backd/docs/auth/audit/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/audit/</guid><description>&lt;p&gt;Each realm with &lt;code&gt;auth: enabled&lt;/code&gt; keeps an &lt;strong&gt;audit trail&lt;/strong&gt;: one record per security-sensitive action or event, with who did it, to what, when and from where. It answers questions such as &amp;ldquo;who gave Bob the admin role?&amp;rdquo; or &amp;ldquo;which key created this user?&amp;rdquo;. Records can be read, but not changed or deleted: they expire after the realm&amp;rsquo;s retention period.&lt;/p&gt;
&lt;h2 id="what-is-recorded"&gt;What is recorded&lt;a class="hd-anchor" href="#what-is-recorded" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Action&lt;/th&gt;
 &lt;th&gt;When&lt;/th&gt;
 &lt;th&gt;Details&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.create&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator (or &lt;code&gt;backd bootstrap&lt;/code&gt;) creates a user&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;password&lt;/code&gt; (whether one was set), &lt;code&gt;roles&lt;/code&gt; (seeded from &lt;code&gt;realm.yaml&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.signup&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user signs up&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;invited&lt;/code&gt;, &lt;code&gt;roles&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.password&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator sets a user&amp;rsquo;s password&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.password_change&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user changes their own password&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.verify_email&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator marks an email as verified, or not; or a user verifies it with the link in their email&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;verified&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.password_reset&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user sets a new password with a reset link&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;verified_address&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.email_changed&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user&amp;rsquo;s address changes: confirmed by the new address, or set by an administrator&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;by&lt;/code&gt; (&lt;code&gt;self&lt;/code&gt; or &lt;code&gt;admin&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.email_change_reverted&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The old address undid a change&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.purged_unverified&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A worker deletes accounts that never verified their address&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;count&lt;/code&gt;, &lt;code&gt;older_than&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;function.invoke_manual&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator runs a function by hand (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;as&lt;/code&gt; (the user&amp;rsquo;s &lt;strong&gt;id&lt;/strong&gt;, never their email, or null)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;files.versions.regenerate&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator runs &lt;a href="../../files/file-fields/#after-you-change-a-declaration"&gt;&lt;code&gt;backd files regenerate&lt;/code&gt;&lt;/a&gt; (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;version&lt;/code&gt;, &lt;code&gt;missing_only&lt;/code&gt;, &lt;code&gt;job_id&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;storage.reconcile&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator runs &lt;a href="../../files/maintenance/#reconciling"&gt;&lt;code&gt;backd storage reconcile&lt;/code&gt;&lt;/a&gt; (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;orphans&lt;/code&gt; (how many were found), &lt;code&gt;deleted&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;storage.check&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator runs a &lt;a href="../../files/storage/#checking-it"&gt;storage check&lt;/a&gt; (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;); never the keys or anything stored&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;provider&lt;/code&gt;, &lt;code&gt;bucket&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;data.check&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator starts a &lt;a href="../../configuration/validation/#finding-documents-that-no-longer-match"&gt;schema check&lt;/a&gt; (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;); the target is &lt;code&gt;data:&amp;lt;database&amp;gt;/&amp;lt;collection&amp;gt;&lt;/code&gt;, &lt;code&gt;data:&amp;lt;database&amp;gt;&lt;/code&gt; or &lt;code&gt;data:*&lt;/code&gt;; never the report&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;collections&lt;/code&gt;, &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;job_id&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;schedule.pause&lt;/code&gt;, &lt;code&gt;schedule.resume&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator pauses or resumes a function&amp;rsquo;s schedule (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;); only a change is recorded; the target is &lt;code&gt;schedule:&amp;lt;database&amp;gt;/&amp;lt;function&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;job.cancel&lt;/code&gt;, &lt;code&gt;job.rerun&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator cancels a job, or queues a finished one again (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;); the target is &lt;code&gt;job:&amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;function&lt;/code&gt;; for a re-run also &lt;code&gt;new_job&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.disable&lt;/code&gt;, &lt;code&gt;user.enable&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator disables or re-enables a user&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.delete&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator erases a user (the tombstone is made; the data work is in the job)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;job_id&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.erased&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The erase job finished, or failed for good&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;job_id&lt;/code&gt; and &lt;code&gt;counts&lt;/code&gt; per &lt;code&gt;database/collection/operation&lt;/code&gt;; &lt;code&gt;needs_attention&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; when it failed. Never content&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.delete_account&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user deletes their own account (it is deactivated, not erased)&lt;/td&gt;
 &lt;td&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;user.networks&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user&amp;rsquo;s &lt;a href="../../configuration/realm/#network-restrictions"&gt;network restrictions&lt;/a&gt; change, through the API or from &lt;code&gt;realm.yaml&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;admin_networks&lt;/code&gt;, &lt;code&gt;login_networks&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;role.add&lt;/code&gt;, &lt;code&gt;role.remove&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A role is assigned or taken away, through the API or from &lt;code&gt;realm.yaml&lt;/code&gt; seeds&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;role&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;apikey.create&lt;/code&gt;, &lt;code&gt;apikey.revoke&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An API key is created or revoked&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;role&lt;/code&gt;, &lt;code&gt;networks&lt;/code&gt;, &lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;invitation.create&lt;/code&gt;, &lt;code&gt;invitation.revoke&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An invitation is created or revoked&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;bound_to_email&lt;/code&gt; (whether it is), &lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;invitation.sent&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An invitation is emailed to its address&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;expires_at&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;admin.login&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A user holding an admin role logs in&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;session_id&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;data.create&lt;/code&gt;, &lt;code&gt;data.update&lt;/code&gt;, &lt;code&gt;data.delete&lt;/code&gt;, &lt;code&gt;data.restore&lt;/code&gt;, &lt;code&gt;data.purge&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A document is written through the &lt;a href="../admin/#data"&gt;admin data route&lt;/a&gt; (a batch audits each operation)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;database&lt;/code&gt;, &lt;code&gt;collection&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;; the target is &lt;code&gt;doc:&amp;lt;database&amp;gt;/&amp;lt;collection&amp;gt;/&amp;lt;id&amp;gt;&lt;/code&gt;; never content&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;session.revoke&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An administrator ends a user&amp;rsquo;s session (&lt;a href="../admin/"&gt;admin API&lt;/a&gt;)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;session&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;admin.refused&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;An admin API request is refused: by a network restriction, or because the administrator&amp;rsquo;s roles don&amp;rsquo;t open the &lt;a href="../admin/#admin-rights"&gt;area&lt;/a&gt; or hold the rights of the user or role involved&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;realm.bootstrap&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;backd bootstrap&lt;/code&gt; creates the realm&amp;rsquo;s first administrator&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;role&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Failed logins aren&amp;rsquo;t recorded here: they are &lt;a href="../sessions/#brute-force-protection"&gt;throttled&lt;/a&gt; and counted, and every request is in the access log.&lt;/p&gt;</description></item><item><title>Secrets</title><link>https://fernandezvara.github.io/backd/docs/functions/secrets/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/secrets/</guid><description>&lt;p&gt;A function that needs a third-party API key, or any value it shouldn&amp;rsquo;t hold in its own code, declares it in &lt;code&gt;function.yaml&lt;/code&gt;&amp;rsquo;s &lt;code&gt;secrets&lt;/code&gt; list and reads it from &lt;code&gt;ctx.secrets&lt;/code&gt;, decrypted, at call time — never from an environment variable, which the function&amp;rsquo;s process doesn&amp;rsquo;t have.&lt;/p&gt;
&lt;aside class="hd-hint hd-hint--tip"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;path d="M9 18h6M10 21h4"/&gt;&lt;path d="M12 3a6 6 0 0 0-4 10.5c.7.7 1 1.5 1 2.5h6c0-1 .3-1.8 1-2.5A6 6 0 0 0 12 3Z"/&gt;&lt;/svg&gt;
Best practice&lt;/p&gt;</description></item><item><title>Outbound network</title><link>https://fernandezvara.github.io/backd/docs/functions/network/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/network/</guid><description>&lt;h2 id="calling-an-outside-api"&gt;Calling an outside API&lt;a class="hd-anchor" href="#calling-an-outside-api" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A function reaches the outside world only for hosts it declares. Say it needs to email a digest through a provider&amp;rsquo;s HTTP API: declare the host, declare the key as a &lt;a href="../secrets/"&gt;secret&lt;/a&gt;, and use &lt;code&gt;fetch&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# function.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;async&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;@daily&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;api.mailer.example] &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# host or host:port, lower case; no scheme, path or wildcard&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;MAILER_KEY]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://api.mailer.example/v1/send&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content-type&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Bearer &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAILER_KEY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;body&lt;/span&gt;: &lt;span class="kt"&gt;JSON.stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ops@example.com&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`Digest &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt; orders, &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;revenue&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt; in revenue`&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;424&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mailer_failed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`the mail provider answered &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;A request to any host not in &lt;code&gt;network&lt;/code&gt; fails, and so does one to a private, loopback or link-local address, even for a declared host name that resolves to one.&lt;/li&gt;
&lt;li&gt;Without a &lt;code&gt;network&lt;/code&gt; list, a function can only call &lt;code&gt;backd&lt;/code&gt; itself (through &lt;code&gt;ctx.db&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;This is how the &lt;a href="../cron/#another-example-a-daily-digest"&gt;cookbook&amp;rsquo;s &lt;code&gt;daily_digest&lt;/code&gt;&lt;/a&gt; would send its result by email instead of storing it.&lt;/li&gt;
&lt;li&gt;Send an idempotency key to the provider when the call has side effects and the function can run twice (&lt;a href="../jobs/#designing-jobs-that-are-safe-to-repeat"&gt;Async jobs&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="egress-the-network-allowlist-completed"&gt;Egress: the network allowlist, completed&lt;a class="hd-anchor" href="#egress-the-network-allowlist-completed" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Why this exists.&lt;/strong&gt; A function runs code you may not have written yourself (an npm dependency, a library update). If its outbound network were left to a single hostname allowlist, a bug or a compromised dependency could still: connect to MongoDB or another private service directly; reach your cloud provider&amp;rsquo;s metadata endpoint (&lt;code&gt;169.254.169.254&lt;/code&gt;) and steal the credentials your infrastructure runs as (SSRF); or reach the executor&amp;rsquo;s or &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s own internal endpoints. A hostname allowlist alone doesn&amp;rsquo;t stop any of this, because it checks &lt;strong&gt;names&lt;/strong&gt;, not the addresses they resolve to — an allowed name can resolve to a private or loopback address (by mistake, by DNS rebinding, or because an attacker controls that DNS record) — and a raw TCP connection (&lt;code&gt;Deno.connect&lt;/code&gt;, used directly instead of &lt;code&gt;fetch&lt;/code&gt;) never goes through an HTTP proxy at all, so a proxy-based check alone can&amp;rsquo;t see it either.&lt;/p&gt;</description></item><item><title>Build, dev and test</title><link>https://fernandezvara.github.io/backd/docs/functions/testing/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/testing/</guid><description>&lt;h2 id="building"&gt;Building&lt;a class="hd-anchor" href="#building" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Functions are bundled before &lt;code&gt;backd&lt;/code&gt; starts: each function, with everything it imports (&lt;code&gt;lib/&lt;/code&gt;, npm and JSR packages), becomes one JavaScript file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;CONFIG_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./config backd functions build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# bundled blog/main/stats → stats-3f2a9c0d1e4b5a6c.js (1843 bytes)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;On Windows and macOS&lt;/strong&gt;, &lt;code&gt;backd&lt;/code&gt; itself runs, but its executor doesn&amp;rsquo;t (see &lt;a href="../running/#what-runs-where"&gt;what runs where&lt;/a&gt;): develop with the local stack&amp;rsquo;s containers, which include one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The build runs &lt;code&gt;deno bundle&lt;/code&gt; and needs &lt;strong&gt;Deno 2.9.7&lt;/strong&gt; on the &lt;code&gt;PATH&lt;/code&gt; (or the binary named by &lt;code&gt;DENO&lt;/code&gt;); it refuses any other version, because bundles must match the Deno that runs them. Without Deno installed, run it in the official Deno image with a Linux &lt;code&gt;backd&lt;/code&gt; binary (from the release archives):&lt;/p&gt;</description></item><item><title>Running in production</title><link>https://fernandezvara.github.io/backd/docs/functions/running/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/running/</guid><description>&lt;h2 id="what-runs-where"&gt;What runs where&lt;a class="hd-anchor" href="#what-runs-where" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Functions add up to four kinds of process. All are the same &lt;code&gt;backd&lt;/code&gt; binary (the executor&amp;rsquo;s image adds Deno), each in its own role:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Process&lt;/th&gt;
 &lt;th&gt;Command&lt;/th&gt;
 &lt;th&gt;Needed for&lt;/th&gt;
 &lt;th&gt;How many&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;API&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;backd serve&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;calling functions over HTTP&lt;/td&gt;
 &lt;td&gt;as many as you like, all with the same config&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Executor&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;backd executor&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;running any function&lt;/td&gt;
 &lt;td&gt;one or more, reachable only from &lt;code&gt;backd&lt;/code&gt; and workers&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Worker&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;backd worker&lt;/code&gt;, or &lt;code&gt;backd serve --with-worker&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;a href="../jobs/"&gt;async jobs&lt;/a&gt; and &lt;a href="../cron/"&gt;cron schedules&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;one or more; each job and each scheduled run is done by exactly one&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Egress&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;backd egress&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;functions that declare &lt;a href="../network/"&gt;&lt;code&gt;network&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;one or more; the executor&amp;rsquo;s only way out&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class="hd-hint hd-hint--note"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;circle cx="12" cy="12" r="9"/&gt;&lt;path d="M12 16v-4m0-4h.01"/&gt;&lt;/svg&gt;
Note&lt;/p&gt;</description></item><item><title>Function logs</title><link>https://fernandezvara.github.io/backd/docs/functions/logs/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/logs/</guid><description>&lt;p&gt;Every function call — &lt;code&gt;sync&lt;/code&gt;, &lt;code&gt;webhook&lt;/code&gt;, and once an &lt;a href="../jobs/"&gt;async&lt;/a&gt; job finishes — is recorded in &lt;code&gt;&amp;lt;realm&amp;gt;___system.invocations&lt;/code&gt;: what happened, not what was sent or returned. &lt;code&gt;backd&lt;/code&gt; is a short debugging window, not a log archive — plan to ship logs elsewhere for anything you need to keep.&lt;/p&gt;
&lt;h2 id="whats-recorded"&gt;What&amp;rsquo;s recorded&lt;a class="hd-anchor" href="#whats-recorded" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The function (&lt;code&gt;&amp;lt;database&amp;gt;/&amp;lt;name&amp;gt;&lt;/code&gt;), who called it, its mode, status and duration.&lt;/li&gt;
&lt;li&gt;The function&amp;rsquo;s own console output (&lt;code&gt;console.log&lt;/code&gt;, &lt;code&gt;console.error&lt;/code&gt;, …), already masked of any &lt;a href="../secrets/"&gt;declared secrets&amp;rsquo;&lt;/a&gt; exact values.&lt;/li&gt;
&lt;li&gt;The &lt;a href="../jobs/#reporting-progress-from-a-long-job"&gt;steps&lt;/a&gt; the function reported with &lt;code&gt;ctx.step()&lt;/code&gt;, closed when the call ended (at most 100).&lt;/li&gt;
&lt;li&gt;The request id, so a record can be matched to &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s own JSON log line for the same call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never&lt;/strong&gt; the input or output. If you need to reproduce what a specific call did, log what matters yourself with &lt;code&gt;console.log&lt;/code&gt; inside the function.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;d3c9ljp8hc2g00b6s1m0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;at&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2026-09-29T12:00:03.500Z&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;orders/checkout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;actor&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user:d3c9lg38hc2g00b6s1lg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sync&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;duration_ms&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;request_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;darf9mq5mh5g00aq9df0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;job_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;parent_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;origin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;logs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;level&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;log&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;line&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;charging card for 1250&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;steps&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;steps_omitted&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;status&lt;/code&gt; is one of &lt;code&gt;ok&lt;/code&gt;, &lt;code&gt;function_error&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;memory&lt;/code&gt;, &lt;code&gt;cpu&lt;/code&gt;, &lt;code&gt;crash&lt;/code&gt;, &lt;code&gt;output_too_large&lt;/code&gt;, &lt;code&gt;busy&lt;/code&gt;, &lt;code&gt;bundle&lt;/code&gt; or &lt;code&gt;cancelled&lt;/code&gt; (an async job&amp;rsquo;s attempt that an administrator &lt;a href="../jobs/#cancelling-and-re-running-a-job"&gt;cancelled&lt;/a&gt; while it ran) — the same reasons a call can end that show up in &lt;code&gt;backd&lt;/code&gt;&amp;rsquo;s own log and in the &lt;a href="../calling/#calling-a-function"&gt;error table&lt;/a&gt;. &lt;code&gt;code&lt;/code&gt; is the function&amp;rsquo;s own error code (&lt;code&gt;ctx.error&lt;/code&gt;&amp;rsquo;s second argument) when &lt;code&gt;status&lt;/code&gt; is &lt;code&gt;function_error&lt;/code&gt;. For an &lt;a href="../jobs/"&gt;async&lt;/a&gt; call, &lt;code&gt;mode&lt;/code&gt; is &lt;code&gt;async&lt;/code&gt; and &lt;code&gt;job_id&lt;/code&gt; names the job; the record is written once the job finishes, not when it&amp;rsquo;s queued.&lt;/p&gt;</description></item><item><title>Cookbook</title><link>https://fernandezvara.github.io/backd/docs/functions/cookbook/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/cookbook/</guid><description>&lt;p&gt;Every recipe here is a function of the &lt;code&gt;workshop&lt;/code&gt; realm (&lt;a href="https://github.com/fernandezvara/backd/tree/main/examples/config/workshop"&gt;&lt;code&gt;examples/config/workshop&lt;/code&gt;&lt;/a&gt; in the repository): a tiny shop with &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;refunds&lt;/code&gt;, &lt;code&gt;reports&lt;/code&gt;, &lt;code&gt;digests&lt;/code&gt; and &lt;code&gt;events&lt;/code&gt;. The code on this page is read straight from those files, and &lt;code&gt;TestWorkshopExample&lt;/code&gt; runs every function for real (MongoDB, a real executor, a real worker), so what you read is what runs.&lt;/p&gt;
&lt;h2 id="run-the-workshop"&gt;Run the workshop&lt;a class="hd-anchor" href="#run-the-workshop" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;



&lt;p&gt;The repository&amp;rsquo;s local stack serves the realm, and its &lt;code&gt;backd&lt;/code&gt; runs a worker (&lt;code&gt;serve --with-worker&lt;/code&gt;), so jobs and schedules work:&lt;/p&gt;</description></item><item><title>Deleting and erasing users</title><link>https://fernandezvara.github.io/backd/docs/auth/erasure/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/erasure/</guid><description>&lt;p&gt;&lt;code&gt;backd&lt;/code&gt; separates two things people call &amp;ldquo;deleting an account&amp;rdquo;:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;What&lt;/th&gt;
 &lt;th&gt;Who&lt;/th&gt;
 &lt;th&gt;Effect&lt;/th&gt;
 &lt;th&gt;Reversible&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Deactivate&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;the user (&lt;code&gt;DELETE /_auth/me&lt;/code&gt;, with their password), or an administrator (&lt;code&gt;PATCH /_admin/users/{id}&lt;/code&gt; with &lt;code&gt;disabled&lt;/code&gt;, &lt;code&gt;backd user disable&lt;/code&gt;)&lt;/td&gt;
 &lt;td&gt;the user is disabled and every session ends; &lt;strong&gt;all data is kept&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;yes: an administrator reactivates (&lt;code&gt;backd user enable&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Erase&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;an administrator (&lt;code&gt;DELETE /_admin/users/{id}&lt;/code&gt;, &lt;code&gt;backd user delete --yes&lt;/code&gt;)&lt;/td&gt;
 &lt;td&gt;the user becomes a tombstone, and the policies of your collections are applied (below)&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class="hd-hint hd-hint--note"&gt;
 &lt;p class="hd-hint-title"&gt;&lt;svg class="hd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"&gt;&lt;circle cx="12" cy="12" r="9"/&gt;&lt;path d="M12 16v-4m0-4h.01"/&gt;&lt;/svg&gt;
Note&lt;/p&gt;</description></item><item><title>Files in functions</title><link>https://fernandezvara.github.io/backd/docs/functions/files/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/functions/files/</guid><description>&lt;p&gt;A function reaches the files of a document through the same client it uses for data: &lt;code&gt;files(id, field)&lt;/code&gt; on a collection of &lt;code&gt;ctx.db&lt;/code&gt; or &lt;code&gt;ctx.admin.db&lt;/code&gt;. It makes the same requests as any client, to the &lt;a href="../../files/transfers/"&gt;&lt;code&gt;_files&lt;/code&gt; routes&lt;/a&gt;, so the same limits, type detection, journal and usage totals apply.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;main&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;people&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;personId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;photo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ada.png&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;image/png&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// the updated document
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// { file, response, bytes(), text() }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// { url, expires_at }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// the field&amp;#39;s files; delete(fileId) removes one
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;put(data, { name, type, ifMatch })&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Stores a file (a string, bytes, a &lt;code&gt;Blob&lt;/code&gt; or a stream): it replaces a single field&amp;rsquo;s file and is added to a &lt;code&gt;multiple&lt;/code&gt; field&amp;rsquo;s. Resolves with the document, which now holds the file&amp;rsquo;s details. &lt;code&gt;type&lt;/code&gt; is a hint: backd detects the real one from the content&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;get(fileId?)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Downloads a file. Without &lt;code&gt;fileId&lt;/code&gt;, the one a single field holds; a field with several needs one. The bytes come through backd, even where downloads are redirects for everyone else, because a function can&amp;rsquo;t reach the bucket&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;link(fileId?)&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;{ url, expires_at }&lt;/code&gt;, exactly like &lt;code&gt;?link=json&lt;/code&gt;: for the app to open. A link of the storage is for the bucket; a &lt;a href="../../files/transfers/#links-for-apps"&gt;backd link&lt;/a&gt; needs &lt;code&gt;BACKD_URL&lt;/code&gt;, so it can name backd&amp;rsquo;s public address&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;delete(fileId?, { ifMatch })&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Removes one file, or every file of the field. Resolves with the document&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="as-the-caller-or-as-the-function"&gt;As the caller, or as the function&lt;a class="hd-anchor" href="#as-the-caller-or-as-the-function" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ctx.db&lt;/code&gt; acts as the caller: &lt;code&gt;put&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; are asked of the document&amp;rsquo;s &lt;code&gt;update&lt;/code&gt; rule (with &lt;code&gt;data&lt;/code&gt; and &lt;code&gt;changed()&lt;/code&gt; as for an upload), &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;link&lt;/code&gt; of the &lt;code&gt;read&lt;/code&gt; rule, and an upload is recorded as theirs. A document the caller can&amp;rsquo;t read is a &lt;code&gt;NotFoundError&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Connecting storage</title><link>https://fernandezvara.github.io/backd/docs/files/storage/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/storage/</guid><description>&lt;p&gt;A realm connects its storage with a &lt;code&gt;storage:&lt;/code&gt; section in its &lt;a href="../../configuration/realm/"&gt;&lt;code&gt;realm.yaml&lt;/code&gt;&lt;/a&gt;. It needs &lt;code&gt;auth&lt;/code&gt; enabled, like everything that keeps secrets in the realm&amp;rsquo;s system database.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minio &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# aws | minio | r2 | digitalocean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://files.example.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;us-east-1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prod &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# required; unique per backd instance sharing the bucket&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;access_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_ACCESS_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_SECRET_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;download&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;presigned &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# presigned | proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;presigned_ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;5m&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;pending_ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1h&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;realm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;10GiB &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# optional&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1GiB &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# optional&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Setting&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;provider&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Required.&lt;/strong&gt; One of &lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;minio&lt;/code&gt;, &lt;code&gt;r2&lt;/code&gt;, &lt;code&gt;digitalocean&lt;/code&gt;. Anything else stops &lt;code&gt;backd&lt;/code&gt; at startup.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;endpoint&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Only the scheme and host, &lt;code&gt;https://&amp;lt;host&amp;gt;[:port]&lt;/code&gt;; the bucket goes in &lt;code&gt;bucket&lt;/code&gt;, never in the endpoint. Required for &lt;code&gt;minio&lt;/code&gt; and &lt;code&gt;r2&lt;/code&gt;; &lt;code&gt;aws&lt;/code&gt; and &lt;code&gt;digitalocean&lt;/code&gt; derive it from &lt;code&gt;region&lt;/code&gt;. It must have the shape the provider&amp;rsquo;s &lt;a href="../"&gt;page&lt;/a&gt; says, or startup stops and names the setting.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;public_endpoint&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;&lt;code&gt;minio&lt;/code&gt; only.&lt;/strong&gt; The address signed links are made for when browsers can&amp;rsquo;t reach &lt;code&gt;endpoint&lt;/code&gt; (&lt;code&gt;backd&lt;/code&gt; at &lt;code&gt;http://minio:9000&lt;/code&gt;, browsers at &lt;code&gt;http://localhost:9000&lt;/code&gt;). &lt;code&gt;backd&lt;/code&gt; never connects to it: it is only written into links.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;region&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;aws&lt;/code&gt; and &lt;code&gt;digitalocean&lt;/code&gt; read it from the endpoint (and refuse one that disagrees); &lt;code&gt;r2&lt;/code&gt; is always &lt;code&gt;auto&lt;/code&gt;; &lt;code&gt;minio&lt;/code&gt; accepts any (default &lt;code&gt;us-east-1&lt;/code&gt;).&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;bucket&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The bucket&amp;rsquo;s name, which must already exist.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;prefix&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Required.&lt;/strong&gt; Starts every object key (&lt;code&gt;&amp;lt;prefix&amp;gt;/&amp;lt;realm&amp;gt;/…&lt;/code&gt;). See below.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;access_key&lt;/code&gt;, &lt;code&gt;secret_key&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;Required&lt;/strong&gt;, written &lt;code&gt;secret:NAME&lt;/code&gt;: the names of two &lt;a href="../../functions/secrets/"&gt;realm secrets&lt;/a&gt;, never the keys themselves.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;download&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The realm&amp;rsquo;s default for how files are delivered: &lt;code&gt;presigned&lt;/code&gt; (a redirect to a signed link, the default) or &lt;code&gt;proxy&lt;/code&gt; (streamed through &lt;code&gt;backd&lt;/code&gt;).&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;presigned_ttl&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;How long a signed link works: &lt;code&gt;10s&lt;/code&gt; to &lt;code&gt;7d&lt;/code&gt;, default &lt;code&gt;5m&lt;/code&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;pending_ttl&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;How long an upload nobody attached to a document is kept: &lt;code&gt;1m&lt;/code&gt; to &lt;code&gt;7d&lt;/code&gt;, default &lt;code&gt;1h&lt;/code&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;quota.realm&lt;/code&gt;, &lt;code&gt;quota.user&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Optional limits on the bytes documents reference: for the whole realm, and for the documents one user owns. Sizes like &lt;code&gt;500MiB&lt;/code&gt; or &lt;code&gt;10GiB&lt;/code&gt;. See &lt;a href="#quotas"&gt;Quotas&lt;/a&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There is no &lt;code&gt;path_style&lt;/code&gt; setting (the provider decides how a bucket is addressed) and no &lt;code&gt;sse&lt;/code&gt; (encryption at rest is the bucket&amp;rsquo;s default). &lt;code&gt;backd template realm&lt;/code&gt; writes the section commented, with an example for each provider.&lt;/p&gt;</description></item><item><title>AWS S3</title><link>https://fernandezvara.github.io/backd/docs/files/aws-s3/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/aws-s3/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;aws&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;eu-west-1 &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# the bucket&amp;#39;s region; the endpoint is derived&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;access_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_ACCESS_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_SECRET_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Endpoint&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://s3.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt; (derived from &lt;code&gt;region&lt;/code&gt;; if you write it, &lt;code&gt;region&lt;/code&gt; must agree)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Addressing&lt;/td&gt;
 &lt;td&gt;virtual-hosted (&lt;code&gt;&amp;lt;bucket&amp;gt;.s3.&amp;lt;region&amp;gt;.amazonaws.com&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Signed SHA-256 (direct uploads)&lt;/td&gt;
 &lt;td&gt;supported&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Largest single PUT&lt;/td&gt;
 &lt;td&gt;5 GiB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Multipart uploads (files above 5 GiB)&lt;/td&gt;
 &lt;td&gt;supported, with a SHA-256 per part&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backd storage check&lt;/code&gt; reads&lt;/td&gt;
 &lt;td&gt;CORS and default encryption&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="setting-it-up"&gt;Setting it up&lt;a class="hd-anchor" href="#setting-it-up" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the bucket&lt;/strong&gt; in the region you will name, with &lt;em&gt;Block all public access&lt;/em&gt; on. The bucket is always private.&lt;/p&gt;</description></item><item><title>MinIO</title><link>https://fernandezvara.github.io/backd/docs/files/minio/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/minio/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;minio&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://minio.internal.example:9000 &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# http://minio:9000 only with BACKD_DEV=true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# public_endpoint: http://localhost:9000 # when browsers reach MinIO at another address&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;access_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_ACCESS_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_SECRET_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Endpoint&lt;/td&gt;
 &lt;td&gt;any &lt;code&gt;https://&amp;lt;host&amp;gt;[:port]&lt;/code&gt; (plain &lt;code&gt;http&lt;/code&gt; only when &lt;code&gt;BACKD_DEV=true&lt;/code&gt;, for development)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Addressing&lt;/td&gt;
 &lt;td&gt;path style (&lt;code&gt;&amp;lt;endpoint&amp;gt;/&amp;lt;bucket&amp;gt;/…&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Region&lt;/td&gt;
 &lt;td&gt;any; default &lt;code&gt;us-east-1&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Signed SHA-256 (direct uploads)&lt;/td&gt;
 &lt;td&gt;supported&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Multipart uploads (files above 5 GiB)&lt;/td&gt;
 &lt;td&gt;supported, with a SHA-256 per part&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Oldest version tested&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;RELEASE.2025-09-07T16-13-09Z&lt;/code&gt; (the repository&amp;rsquo;s tests run against Chainguard&amp;rsquo;s current build as well)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backd storage check&lt;/code&gt; reads&lt;/td&gt;
 &lt;td&gt;neither CORS nor encryption (MinIO doesn&amp;rsquo;t expose them to an S3 client): it says so&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="development"&gt;Development&lt;a class="hd-anchor" href="#development" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Images:&lt;/strong&gt; the MinIO project no longer publishes container images (the old &lt;code&gt;minio/minio&lt;/code&gt; and &lt;code&gt;quay.io/minio/minio&lt;/code&gt; repositories stopped being pullable). The repository&amp;rsquo;s stacks use &lt;a href="https://images.chainguard.dev/directory/image/minio/overview"&gt;Chainguard&amp;rsquo;s build&lt;/a&gt; (&lt;code&gt;cgr.dev/chainguard/minio&lt;/code&gt; and &lt;code&gt;cgr.dev/chainguard/minio-client&lt;/code&gt;); both have no shell, so the server can&amp;rsquo;t run a health check and one-shot jobs call &lt;code&gt;mc&lt;/code&gt; directly. You can also build MinIO from its source. &lt;code&gt;backd storage check&lt;/code&gt; tells whether whatever you run behaves.&lt;/p&gt;</description></item><item><title>Cloudflare R2</title><link>https://fernandezvara.github.io/backd/docs/files/cloudflare-r2/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/cloudflare-r2/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;r2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://&amp;lt;account id&amp;gt;.r2.cloudflarestorage.com &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# no bucket in it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;access_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_ACCESS_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_SECRET_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Endpoint&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://&amp;lt;account id&amp;gt;.r2.cloudflarestorage.com&lt;/code&gt; (also &lt;code&gt;.eu.&lt;/code&gt; and &lt;code&gt;.fedramp.&lt;/code&gt; for those jurisdictions). R2&amp;rsquo;s console shows it with the bucket&amp;rsquo;s name after it: write only the host, and the name in &lt;code&gt;bucket&lt;/code&gt;.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Addressing&lt;/td&gt;
 &lt;td&gt;virtual-hosted&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Region&lt;/td&gt;
 &lt;td&gt;always &lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Signed SHA-256 (direct uploads)&lt;/td&gt;
 &lt;td&gt;supported: R2 rejects an upload whose digest differs (confirmed by the provider smoke test)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Largest single PUT&lt;/td&gt;
 &lt;td&gt;5 GiB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Multipart uploads (files above 5 GiB)&lt;/td&gt;
 &lt;td&gt;supported, with a SHA-256 per part (confirmed by the provider smoke test)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backd storage check&lt;/code&gt; reads&lt;/td&gt;
 &lt;td&gt;neither CORS nor encryption: it says so&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="setting-it-up"&gt;Setting it up&lt;a class="hd-anchor" href="#setting-it-up" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create the bucket&lt;/strong&gt; in the Cloudflare dashboard (R2 → Create bucket). Keep it private: don&amp;rsquo;t enable public access or a public domain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create an API token&lt;/strong&gt; (R2 → Manage API tokens) with &lt;em&gt;Object Read &amp;amp; Write&lt;/em&gt; limited to that bucket. R2 shows an &lt;strong&gt;Access Key ID&lt;/strong&gt; and a &lt;strong&gt;Secret Access Key&lt;/strong&gt; once: those are the two secrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption at rest:&lt;/strong&gt; R2 encrypts all objects at rest; there is nothing to set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS&lt;/strong&gt;, needed for &lt;a href="../transfers/#direct-uploads"&gt;direct uploads&lt;/a&gt; and cross-origin downloads: set it on the bucket (Settings → CORS policy) for your app&amp;rsquo;s origin, with &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;HEAD&lt;/code&gt; and the allowed headers &lt;code&gt;Content-Type&lt;/code&gt; and &lt;code&gt;x-amz-checksum-sha256&lt;/code&gt;. &lt;code&gt;backd storage check&lt;/code&gt; can&amp;rsquo;t read it: try an upload from the app.&lt;/li&gt;
&lt;li&gt;Set the secrets and run &lt;code&gt;backd storage check --realm &amp;lt;realm&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;R2&amp;rsquo;s tokens limit the bucket, not a prefix: the &lt;code&gt;prefix&lt;/code&gt; still keeps each &lt;code&gt;backd&lt;/code&gt; instance&amp;rsquo;s files apart, but a key can reach the whole bucket, so give each instance sharing a bucket a token of its own only if you must trust no instance with another&amp;rsquo;s files; otherwise use a bucket per instance.&lt;/p&gt;</description></item><item><title>DigitalOcean Spaces</title><link>https://fernandezvara.github.io/backd/docs/files/digitalocean-spaces/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/digitalocean-spaces/</guid><description>&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;digitalocean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nyc3 &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# the Space&amp;#39;s region; the endpoint is derived&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;acme-files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;prod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;access_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_ACCESS_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;secret_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;secret:STORAGE_SECRET_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;th&gt;&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Endpoint&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;https://&amp;lt;region&amp;gt;.digitaloceanspaces.com&lt;/code&gt; (derived from &lt;code&gt;region&lt;/code&gt;; if you write it, &lt;code&gt;region&lt;/code&gt; must agree)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Addressing&lt;/td&gt;
 &lt;td&gt;virtual-hosted&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Signed SHA-256 (direct uploads)&lt;/td&gt;
 &lt;td&gt;supported (documented in the &lt;a href="https://docs.digitalocean.com/reference/api/spaces/"&gt;Spaces API reference&lt;/a&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Largest single PUT&lt;/td&gt;
 &lt;td&gt;5 GiB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Multipart uploads (files above 5 GiB)&lt;/td&gt;
 &lt;td&gt;not enabled until the provider smoke test confirms a SHA-256 per part: &lt;code&gt;max_size&lt;/code&gt; stays at 5 GiB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backd storage check&lt;/code&gt; reads&lt;/td&gt;
 &lt;td&gt;neither CORS nor encryption: it says so&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="setting-it-up"&gt;Setting it up&lt;a class="hd-anchor" href="#setting-it-up" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create the Space&lt;/strong&gt; in the region you will name, with &lt;em&gt;File listing&lt;/em&gt; restricted (private).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a Spaces access key&lt;/strong&gt; (API → Spaces Keys), scoped to that Space with &lt;em&gt;Read, Write and Delete&lt;/em&gt;. The key and its secret are the two realm secrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption at rest:&lt;/strong&gt; Spaces encrypts data at rest; there is nothing to set.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CORS&lt;/strong&gt;, needed for &lt;a href="../transfers/#direct-uploads"&gt;direct uploads&lt;/a&gt; and cross-origin downloads: set it in the Space&amp;rsquo;s Settings for your app&amp;rsquo;s origin with &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt; and &lt;code&gt;HEAD&lt;/code&gt;, and allow the headers &lt;code&gt;Content-Type&lt;/code&gt; and &lt;code&gt;x-amz-checksum-sha256&lt;/code&gt;. &lt;code&gt;backd storage check&lt;/code&gt; can&amp;rsquo;t read it: try an upload from the app.&lt;/li&gt;
&lt;li&gt;Set the secrets and run &lt;code&gt;backd storage check --realm &amp;lt;realm&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A Spaces key is scoped to a Space, not to a prefix: use a Space per &lt;code&gt;backd&lt;/code&gt; instance, or keep &lt;code&gt;prefix&lt;/code&gt; distinct and accept that a key can reach every prefix of its Space.&lt;/p&gt;</description></item><item><title>File fields</title><link>https://fernandezvara.github.io/backd/docs/files/file-fields/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/file-fields/</guid><description>&lt;p&gt;A collection says which fields of its documents carry files under &lt;code&gt;files:&lt;/code&gt; in its &lt;a href="../../configuration/config-dir/#collectionyaml"&gt;&lt;code&gt;collection.yaml&lt;/code&gt;&lt;/a&gt;. The realm needs a &lt;a href="../storage/"&gt;&lt;code&gt;storage:&lt;/code&gt;&lt;/a&gt; section.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;2MiB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;image/png, image/jpeg, image/webp]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;receipts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;10MiB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;application/pdf, image/*]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;manual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;20MiB&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;download&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;proxy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1h&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Key&lt;/th&gt;
 &lt;th&gt;Default&lt;/th&gt;
 &lt;th&gt;Meaning&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_size&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;required&lt;/td&gt;
 &lt;td&gt;The largest one file may be (&lt;code&gt;512KiB&lt;/code&gt;, &lt;code&gt;10MiB&lt;/code&gt;, &lt;code&gt;1GiB&lt;/code&gt;). Checked while the bytes stream in&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;types&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;any&lt;/td&gt;
 &lt;td&gt;Allowed content types, &lt;code&gt;image/png&lt;/code&gt; or a family such as &lt;code&gt;image/*&lt;/code&gt;. Checked against the type &lt;strong&gt;detected from the file&amp;rsquo;s content&lt;/strong&gt;, never the one the client claims. A family never admits SVG, HTML or XML: name them to accept them&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;multiple&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;The field is a list of files instead of one&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_files&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;10&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;With &lt;code&gt;multiple&lt;/code&gt;: how many files the field may hold (1 to 1000)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;upload&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;proxy&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;proxy&lt;/code&gt; streams the bytes through backd; &lt;code&gt;direct&lt;/code&gt; has the client send them straight to the bucket with a signed link, which backd verifies afterwards (&lt;a href="../transfers/#direct-uploads"&gt;Direct uploads&lt;/a&gt;). &lt;code&gt;max_size&lt;/code&gt; is at most 5GiB for &lt;code&gt;direct&lt;/code&gt;, or 4TiB with a provider that takes &lt;a href="../transfers/#files-above-5-gib"&gt;uploads in parts&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;download&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the realm&amp;rsquo;s&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;presigned&lt;/code&gt; redirects to a short-lived link of the storage; &lt;code&gt;proxy&lt;/code&gt; streams the file through backd&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;presigned_ttl&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the realm&amp;rsquo;s&lt;/td&gt;
 &lt;td&gt;How long this field&amp;rsquo;s links last (at most 7 days)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;cache&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;With &lt;code&gt;download: proxy&lt;/code&gt;: how long shared caches may keep the file, only when anyone may read the document&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;keep_metadata&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;By default the metadata of JPEG and PNG pictures is &lt;a href="#metadata-and-the-gps-risk"&gt;removed&lt;/a&gt; when they are stored. &lt;code&gt;true&lt;/code&gt; stores them exactly as sent&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;max_pixels&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;the instance&amp;rsquo;s&lt;/td&gt;
 &lt;td&gt;For images: the most pixels (width × height) the field gets &lt;a href="#image-versions"&gt;versions&lt;/a&gt; for. It can only lower &lt;code&gt;BACKD_IMAGE_MAX_PIXELS&lt;/code&gt;; a higher value stops startup&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;versions&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;none&lt;/td&gt;
 &lt;td&gt;Resized copies of an image, declared by name: see &lt;a href="#image-versions"&gt;Image versions&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A field name is lower-case letters, digits and underscores. &lt;strong&gt;A file field is declared only here:&lt;/strong&gt; one that is also in &lt;code&gt;schema.json&lt;/code&gt; is a startup error, because backd adds its schema itself, to the validators in MongoDB, to the &lt;a href="../rules/"&gt;rules&lt;/a&gt; and to the queries.&lt;/p&gt;</description></item><item><title>Uploads and downloads</title><link>https://fernandezvara.github.io/backd/docs/files/transfers/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/transfers/</guid><description>&lt;p&gt;Every file operation is an operation on a document and follows its &lt;a href="../rules/"&gt;access rules&lt;/a&gt;: the &lt;code&gt;update&lt;/code&gt; rule for uploads and removals, the &lt;code&gt;read&lt;/code&gt; rule for downloads. A document the caller can&amp;rsquo;t read answers &lt;code&gt;404&lt;/code&gt;, never revealing its files. &lt;a href="../../auth/api-keys/"&gt;API keys&lt;/a&gt; and the &lt;a href="../../api/documents/"&gt;admin data route&lt;/a&gt; skip rules, as everywhere.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Operation&lt;/th&gt;
 &lt;th&gt;Request&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Upload&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;POST /v1/{realm}/{database}/{collection}/{id}/_files/{field}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Download&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;GET …/{id}/_files/{field}/{file id}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Remove one&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;DELETE …/{id}/_files/{field}/{file id}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Clear the field&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;DELETE …/{id}/_files/{field}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The same four exist under &lt;code&gt;/v1/{realm}/_admin/data/…&lt;/code&gt;. There is no &lt;code&gt;PUT&lt;/code&gt;: a file is replaced by uploading another.&lt;/p&gt;</description></item><item><title>Rules for files</title><link>https://fernandezvara.github.io/backd/docs/files/rules/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/rules/</guid><description>&lt;p&gt;Files have no rules of their own: they follow the &lt;a href="../../auth/rules/"&gt;rules of their document&lt;/a&gt;.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Operation&lt;/th&gt;
 &lt;th&gt;Rule&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Upload, remove one, clear a field&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Download, &lt;code&gt;?link=json&lt;/code&gt;, &lt;code&gt;?file_links=true&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;read&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;File fields are known to the rules like any other field of the collection, so a rule can read them from &lt;code&gt;document&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt;. For a file operation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;document&lt;/code&gt; is the document as stored; &lt;code&gt;data&lt;/code&gt; is the document &lt;strong&gt;as it would be after the change&lt;/strong&gt;; &lt;code&gt;changed()&lt;/code&gt; lists the changed fields, which includes the file field.&lt;/li&gt;
&lt;li&gt;The rule runs &lt;strong&gt;before any byte is stored&lt;/strong&gt;: a refused upload costs nothing and leaves nothing behind.&lt;/li&gt;
&lt;li&gt;API keys and &lt;code&gt;ctx.admin.db&lt;/code&gt; in functions skip rules.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reserving-a-field-for-functions"&gt;Reserving a field for functions&lt;a class="hd-anchor" href="#reserving-a-field-for-functions" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;A field only a function may set, such as a generated thumbnail, is a file field the rule refuses to clients:&lt;/p&gt;</description></item><item><title>Keeping storage tidy</title><link>https://fernandezvara.github.io/backd/docs/files/maintenance/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/maintenance/</guid><description>&lt;p&gt;backd never lists the bucket while it runs. Instead, every change that stops a document referencing a file &lt;strong&gt;queues that file&amp;rsquo;s object for deletion&lt;/strong&gt;, and the workers (&lt;code&gt;backd worker&lt;/code&gt;, or &lt;code&gt;backd serve --with-worker&lt;/code&gt;) delete it, retrying with a growing wait when the storage fails. An upload that never became part of a document is journaled, and the workers delete its object after a grace period (one hour), or when a pending or direct upload expires.&lt;/p&gt;</description></item><item><title>Files in apps</title><link>https://fernandezvara.github.io/backd/docs/files/apps/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/apps/</guid><description>&lt;p&gt;An app reaches files through the &lt;a href="../../clients/js/#files"&gt;JavaScript client&lt;/a&gt;, which makes the requests described in &lt;a href="../transfers/"&gt;Uploads and downloads&lt;/a&gt;. This page is about what to do with them in a page.&lt;/p&gt;
&lt;h2 id="uploading"&gt;Uploading&lt;a class="hd-anchor" href="#uploading" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;uploadFile(id, field, file, { onProgress })&lt;/code&gt; puts a file in a document, and &lt;code&gt;prepareUpload(field, file)&lt;/code&gt; makes one for a document that doesn&amp;rsquo;t exist yet: name its &lt;code&gt;ref&lt;/code&gt; in the create. Both work whether the field takes uploads through backd or straight to the bucket; the client asks the field (&lt;code&gt;fileField(field)&lt;/code&gt;), which also tells the page &lt;code&gt;max_size&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt; and &lt;code&gt;max_files&lt;/code&gt; to check a file before sending it.&lt;/p&gt;</description></item><item><title>Security of files</title><link>https://fernandezvara.github.io/backd/docs/files/security/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/files/security/</guid><description>&lt;p&gt;The files feature was reviewed as a whole before this page: rules on file operations, storage credentials and the endpoint restriction, both upload modes, type detection, links, proxied downloads, pending uploads and their tokens, the journal and deletion queue, reconcile, erasure and the functions API. This page says what came out of it.&lt;/p&gt;
&lt;h2 id="how-files-are-protected"&gt;How files are protected&lt;a class="hd-anchor" href="#how-files-are-protected" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The bucket is private, and only backd reaches it.&lt;/strong&gt; Files are served through backd, or through a link backd signed for one file. backd connects only to the realm&amp;rsquo;s declared &lt;code&gt;endpoint&lt;/code&gt; (checked after DNS resolution, with no redirects and no proxy); &lt;code&gt;public_endpoint&lt;/code&gt; is only written into links and is never connected to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A file is as private as its document.&lt;/strong&gt; An upload, a removal and a direct-upload start are &lt;code&gt;update&lt;/code&gt; operations; a download, a link and &lt;code&gt;?file_links=true&lt;/code&gt; are &lt;code&gt;read&lt;/code&gt; operations; a document the caller can&amp;rsquo;t read answers &lt;code&gt;404&lt;/code&gt; for all of them, never revealing its files. The rule runs &lt;strong&gt;before any byte is stored&lt;/strong&gt;, with &lt;code&gt;data&lt;/code&gt; as the document will be, and again when the file is attached. See &lt;a href="../rules/"&gt;Rules for files&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clients never write what describes a file.&lt;/strong&gt; The id, name, size, detected type, SHA-256 and time are backd&amp;rsquo;s: a client&amp;rsquo;s write drops them, and a PUT keeps the stored files. The object key is &lt;code&gt;&amp;lt;prefix&amp;gt;/&amp;lt;realm&amp;gt;/&amp;lt;database&amp;gt;/&amp;lt;collection&amp;gt;/&amp;lt;file id&amp;gt;&lt;/code&gt;: no name, and no part of a request, ever reaches it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The type is what the bytes say.&lt;/strong&gt; It is detected from the content, never the client&amp;rsquo;s claim, and &lt;code&gt;types&lt;/code&gt; is checked against it. &lt;strong&gt;A family such as &lt;code&gt;image/*&lt;/code&gt; or &lt;code&gt;text/*&lt;/code&gt; does not admit active content&lt;/strong&gt; (SVG, HTML, XML, scripts): a field takes one only when it names it. A field with no &lt;code&gt;types&lt;/code&gt; takes anything, and serves it only as a download.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Downloads can&amp;rsquo;t run in your origin.&lt;/strong&gt; A presigned link comes from the storage&amp;rsquo;s origin, never backd&amp;rsquo;s, and forces &lt;code&gt;attachment&lt;/code&gt;. A proxied download carries &lt;code&gt;Content-Disposition: attachment&lt;/code&gt;, &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; and &lt;code&gt;Content-Security-Policy: sandbox&lt;/code&gt;. Names are cleaned (no path, no control or direction characters, 255 bytes) and quoted, so a name can&amp;rsquo;t add a header or leave its quotes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct uploads are verified, not trusted.&lt;/strong&gt; The link has the length, the type and the SHA-256 signed in, so the storage refuses another body; completing then checks the size and the checksum the &lt;em&gt;storage&lt;/em&gt; computed (a missing one fails) and detects the type from the first bytes. A mismatch deletes the object. A file sent in &lt;a href="../transfers/#files-above-5-gib"&gt;parts&lt;/a&gt; is verified the same way, part by part and as a whole: every part has its own SHA-256 signed in, and the composite checksum the storage computes is checked against the declared parts. The &lt;code&gt;sha256&lt;/code&gt; recorded for it is that composite&amp;rsquo;s digest, not the SHA-256 of its bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tokens are secrets.&lt;/strong&gt; An upload token is 256 random bits, only its SHA-256 is stored, it is compared in constant time, used once, bound to a collection and field and, when a signed-in user made it, to that user, and never logged. An upload id alone attaches or completes nothing, and every wrong, used, expired or foreign reference answers the same &lt;code&gt;400&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;backd&amp;rsquo;s own links&lt;/strong&gt; are an HMAC over the realm, database, collection, document, field, file and expiry, with a per-realm key, verified in constant time: a link can&amp;rsquo;t be moved to another file or kept past its time. Rotating the key (setting or deleting the secret &lt;code&gt;BACKD_FILES_LINK_KEY&lt;/code&gt;) invalidates every one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backd&amp;rsquo;s keys are not a function&amp;rsquo;s to read.&lt;/strong&gt; A function can&amp;rsquo;t declare the realm&amp;rsquo;s storage secrets or the link key as its own secrets: the configuration is refused at startup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleanup never touches what a document holds.&lt;/strong&gt; A worker checks that no document references an upload before it deletes it, and reconcile only considers objects named like files, older than 24 hours, with no open upload record, and that no declared field references.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Metadata is removed from pictures.&lt;/strong&gt; The GPS position, camera and comments of a JPEG, and the text and time of a PNG, are dropped as it is stored, unless the field sets &lt;code&gt;keep_metadata: true&lt;/code&gt;: see &lt;a href="../file-fields/#metadata-and-the-gps-risk"&gt;Metadata and the GPS risk&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limits&lt;/strong&gt; bound what a caller can store: &lt;code&gt;max_size&lt;/code&gt; and &lt;code&gt;max_files&lt;/code&gt; per field, &lt;code&gt;BACKD_MAX_UPLOAD_BYTES&lt;/code&gt; for proxy uploads, 20 unused uploads per caller (IPv6 callers counted per &lt;code&gt;/64&lt;/code&gt;) and a rate for starting them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-you-are-responsible-for"&gt;What you are responsible for&lt;a class="hd-anchor" href="#what-you-are-responsible-for" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The bucket and its key.&lt;/strong&gt; Keep it private, give the key only what your provider&amp;rsquo;s page lists, on the realm&amp;rsquo;s prefix; encryption at rest is the bucket&amp;rsquo;s setting. &lt;strong&gt;Give each backd instance its own &lt;code&gt;prefix&lt;/code&gt;:&lt;/strong&gt; two instances on one prefix collide, and a &lt;a href="../maintenance/#reconciling"&gt;reconcile&lt;/a&gt; on one deletes the other&amp;rsquo;s files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Links outlive rules.&lt;/strong&gt; A storage link and a backd link both work for anyone who has them until they expire, even if the document&amp;rsquo;s rules change meanwhile. Keep &lt;code&gt;presigned_ttl&lt;/code&gt; short on sensitive fields; the default is five minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caches outlive rules too.&lt;/strong&gt; With &lt;code&gt;download: proxy&lt;/code&gt; and &lt;code&gt;cache&lt;/code&gt;, a shared cache may keep a file of a document anyone can read, and keeps it after you tighten the rule. Use &lt;code&gt;cache&lt;/code&gt; only for files meant to be public.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set &lt;code&gt;BACKD_URL&lt;/code&gt; in production.&lt;/strong&gt; backd-signed links name backd&amp;rsquo;s public address; without it they are built from the request&amp;rsquo;s &lt;code&gt;Host&lt;/code&gt; header, which a caller chooses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anonymous uploads cost storage.&lt;/strong&gt; Where a collection&amp;rsquo;s &lt;code&gt;create&lt;/code&gt; rule admits anonymous callers, each address may hold 20 unused uploads of up to &lt;code&gt;max_size&lt;/code&gt; for an hour. Keep &lt;code&gt;max_size&lt;/code&gt; small on such fields and let the edge rate-limit by address. Nothing scans files for malware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A removed file field leaves trusted-looking data.&lt;/strong&gt; If you take a field out of &lt;code&gt;files:&lt;/code&gt; and later declare it again, what the documents still hold in it is read as file details again. Don&amp;rsquo;t re-use a field name for something else; clear it first.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functions with &lt;code&gt;ctx.admin.db&lt;/code&gt; skip the rules&lt;/strong&gt;, files included: review them as privileged code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backups.&lt;/strong&gt; MongoDB backups don&amp;rsquo;t include files: back the bucket up at the provider and run reconcile after a restore.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Security model</title><link>https://fernandezvara.github.io/backd/docs/auth/security/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/auth/security/</guid><description>&lt;p&gt;This page summarizes how &lt;code&gt;backd&lt;/code&gt; protects data in realms with &lt;code&gt;auth: enabled&lt;/code&gt;, and what the operator is responsible for. To report a vulnerability, see &lt;a href="https://github.com/fernandezvara/backd/blob/main/SECURITY.md"&gt;&lt;code&gt;SECURITY.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="who-can-do-what"&gt;Who can do what&lt;a class="hd-anchor" href="#who-can-do-what" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Caller&lt;/th&gt;
 &lt;th&gt;Proves identity with&lt;/th&gt;
 &lt;th&gt;Can&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Anonymous&lt;/td&gt;
 &lt;td&gt;nothing&lt;/td&gt;
 &lt;td&gt;What &lt;a href="../rules/"&gt;access rules&lt;/a&gt; allow &lt;code&gt;user == nil&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;User&lt;/td&gt;
 &lt;td&gt;session token (&lt;code&gt;bds_…&lt;/code&gt;)&lt;/td&gt;
 &lt;td&gt;What access rules allow that user; manage their own account and sessions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Server-side service&lt;/td&gt;
 &lt;td&gt;API key (&lt;code&gt;bdk_…&lt;/code&gt;) with the &lt;code&gt;data&lt;/code&gt; role&lt;/td&gt;
 &lt;td&gt;Everything in the realm&amp;rsquo;s data; act as a user with &lt;code&gt;X-Backd-On-Behalf-Of&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Management tooling&lt;/td&gt;
 &lt;td&gt;API key with the &lt;code&gt;admin&lt;/code&gt; role&lt;/td&gt;
 &lt;td&gt;The same, plus the &lt;a href="../admin/"&gt;admin API&lt;/a&gt;: users, roles, invitations, API keys&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Administrator&lt;/td&gt;
 &lt;td&gt;session of a user holding an &lt;a href="../../configuration/realm/#roles"&gt;admin role&lt;/a&gt;, for example through &lt;a href="../cli/"&gt;&lt;code&gt;backd login&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;td&gt;What access rules allow that user, plus the admin API&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Operator&lt;/td&gt;
 &lt;td&gt;access to &lt;code&gt;CONFIG_DIR&lt;/code&gt; and MongoDB&lt;/td&gt;
 &lt;td&gt;Everything, including creating a realm&amp;rsquo;s first administrator with &lt;a href="../cli/#the-first-administrator"&gt;&lt;code&gt;backd bootstrap&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Credentials belong to one realm and are unknown in every other realm. Invalid, expired or revoked credentials are always refused (&lt;code&gt;401&lt;/code&gt;), never downgraded to anonymous access.&lt;/p&gt;</description></item><item><title>JavaScript client</title><link>https://fernandezvara.github.io/backd/docs/clients/js/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/clients/js/</guid><description>&lt;p&gt;&lt;code&gt;backd-js&lt;/code&gt; is a small JavaScript library for &lt;code&gt;backd&lt;/code&gt;. It is plain ES modules with no dependencies and no build step, and it runs wherever &lt;code&gt;fetch&lt;/code&gt; does: browsers, Node 20+, Deno, Bun and edge runtimes. It ships TypeScript declarations, so editors autocomplete it in JavaScript and TypeScript projects alike.&lt;/p&gt;
&lt;h2 id="install"&gt;Install&lt;a class="hd-anchor" href="#install" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install backd-js
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It has no dependencies, needs no build step and works with any bundler. Without one, import it in a browser with an &lt;a href="https://developer.mozilla.org/docs/Web/HTML/Element/script/type/importmap"&gt;import map&lt;/a&gt; pointing at the package&amp;rsquo;s &lt;code&gt;src/index.js&lt;/code&gt; (from a CDN such as &lt;code&gt;https://cdn.jsdelivr.net/npm/backd-js/src/index.js&lt;/code&gt;, or from your own server), as the &lt;a href="#example-app"&gt;example app&lt;/a&gt; does.&lt;/p&gt;</description></item><item><title>Blog</title><link>https://fernandezvara.github.io/backd/docs/examples/blog/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/examples/blog/</guid><description>&lt;p&gt;A small blog. Anyone can read published posts. Signed-in users write posts, keep drafts, and edit, publish, unpublish and delete their own. Each post shows when and by whom it was written, and its category. Clicking an author or a category lists only those posts, and both filters combine.&lt;/p&gt;
&lt;p&gt;It shows the everyday use of backd: public and private documents in one collection, ownership, and posts signed with the writer&amp;rsquo;s own identity, all decided on the server by &lt;a href="../../auth/rules/"&gt;access rules&lt;/a&gt;. Unlike the &lt;a href="../expenses/"&gt;expenses example&lt;/a&gt;, it doesn&amp;rsquo;t share documents between users, so rules cover everything it needs: it has no known holes, only the &lt;a href="#limits"&gt;limits&lt;/a&gt; below. One page, above the post list, does need a &lt;a href="#a-server-side-function-stats"&gt;server-side function&lt;/a&gt;: a count of every published post, and of the caller&amp;rsquo;s own, neither computable from what a reader may list.&lt;/p&gt;</description></item><item><title>Expenses without functions</title><link>https://fernandezvara.github.io/backd/docs/examples/expenses/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/examples/expenses/</guid><description>&lt;p&gt;A small group-expenses tracker in the style of Tricount or Splitwise. People create a group, invite others by email, record what they paid, and see who owes whom.&lt;/p&gt;
&lt;p&gt;Unlike the &lt;a href="../blog/"&gt;blog&lt;/a&gt;, where each post belongs to its writer, here documents are shared between users, and some of the checks it needs involve more than one document. It is also a lesson in the limits of access rules. Its &lt;a href="../../auth/rules/"&gt;rules&lt;/a&gt; are as strict as rules can be today, and they still leave holes, because some checks need data from another document or logic on the server. Those holes are documented here on purpose: this is deliberately the &amp;ldquo;before&amp;rdquo; half of a pair. &lt;strong&gt;&lt;a href="../expenses-with-functions/"&gt;Expenses with functions&lt;/a&gt;&lt;/strong&gt; is the &amp;ldquo;after&amp;rdquo; — the same app, same layout, with holes 1–4 closed by &lt;a href="../../functions/"&gt;server-side functions&lt;/a&gt;, compared side by side with what changed and why. Hole 5 is closed by &lt;a href="../../auth/sessions/#email-verification"&gt;email verification&lt;/a&gt; in both: access here is by email address, so the address has to be the user&amp;rsquo;s own.&lt;/p&gt;</description></item><item><title>Expenses with functions</title><link>https://fernandezvara.github.io/backd/docs/examples/expenses-with-functions/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/examples/expenses-with-functions/</guid><description>&lt;p&gt;The same group-expenses tracker as &lt;a href="../expenses/"&gt;expenses without functions&lt;/a&gt;, with the same layout, the same data model&amp;rsquo;s spirit, and the same rules where nothing needed to change — but holes 1 through 4 are closed, by five small &lt;a href="../../functions/"&gt;functions&lt;/a&gt; instead of client-trusted copies. Hole 5 (signing up with someone else&amp;rsquo;s email) is closed too, but by &lt;a href="../../auth/sessions/#email-verification"&gt;email verification&lt;/a&gt;, not by functions: the realm requires a verified address, and rules and functions check &lt;code&gt;user.email_verified&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Read &lt;a href="../expenses/"&gt;expenses without functions&lt;/a&gt; first if you haven&amp;rsquo;t: this page assumes you know the five holes it documents, and spends its time on what&amp;rsquo;s different here and why.&lt;/p&gt;</description></item><item><title>Workshop tour</title><link>https://fernandezvara.github.io/backd/docs/examples/workshop-tour/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/examples/workshop-tour/</guid><description>&lt;p&gt;The &lt;a href="../../functions/cookbook/"&gt;functions cookbook&lt;/a&gt; explains each function of the &lt;code&gt;workshop&lt;/code&gt; realm. The &lt;strong&gt;workshop tour&lt;/strong&gt; lets you use them: one page, one panel per feature, and an &lt;strong&gt;inspector&lt;/strong&gt; beside it that shows every call the page makes, so you see what the browser sent and what &lt;code&gt;backd&lt;/code&gt; answered.&lt;/p&gt;



&lt;p&gt;It is part of the local stack (&lt;code&gt;make example&lt;/code&gt;, then &lt;a href="https://localhost:8443/example/workshop/"&gt;https://localhost:8443/example/workshop/&lt;/a&gt;). The page is plain JavaScript on &lt;a href="../../clients/js/"&gt;&lt;code&gt;backd-js&lt;/code&gt;&lt;/a&gt;, with no build step, in &lt;code&gt;clients/js/examples/workshop&lt;/code&gt;; the realm is &lt;code&gt;examples/config/workshop&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Architecture</title><link>https://fernandezvara.github.io/backd/docs/development/architecture/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/development/architecture/</guid><description>&lt;p&gt;&lt;code&gt;backd&lt;/code&gt; is a single stateless Go service. Apart from the registry built at startup, all state lives in MongoDB, so several identical instances can run side by side.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;CONFIG_DIR ──load──▶ registry ──▶ provisioner ──▶ MongoDB
 │ ▲
HTTP ──▶ handlers ──────┴──▶ storage.Repository ───┘
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="layers"&gt;Layers&lt;a class="hd-anchor" href="#layers" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Package&lt;/th&gt;
 &lt;th&gt;Role&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/settings&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Environment variables → &lt;code&gt;Settings&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/registry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Immutable realm → database → collection tree, with realm settings (&lt;code&gt;realm.yaml&lt;/code&gt;), compiled schemas and field index&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/templates&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Embedded starter files written by &lt;code&gt;backd template&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/rules&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Compiles the &lt;code&gt;rules:&lt;/code&gt; of &lt;code&gt;collection.yaml&lt;/code&gt;: expr-lang rules and runs the static checks (guards, roles, fields, &lt;code&gt;now&lt;/code&gt;, read rules as filters)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/auth&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Storage-neutral identity model (users, identities, sessions), password policy, argon2id hashing with a concurrency cap, sign-up, login and session handling; persistence behind &lt;code&gt;auth.Store&lt;/code&gt; (in-memory fake in &lt;code&gt;internal/auth/authtest&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/query&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Parses the list query language into &lt;code&gt;storage.Condition&lt;/code&gt;s, checking every field and value against the schema&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/storage&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Backend-neutral &lt;code&gt;Repository&lt;/code&gt; interface, &lt;code&gt;Document&lt;/code&gt;/&lt;code&gt;Query&lt;/code&gt;/&lt;code&gt;Page&lt;/code&gt;/&lt;code&gt;Condition&lt;/code&gt;/&lt;code&gt;Filter&lt;/code&gt; types, &lt;code&gt;Fetch&lt;/code&gt; (single reads under a read rule), and the errors &lt;code&gt;ErrNotFound&lt;/code&gt;, &lt;code&gt;ErrVersionMismatch&lt;/code&gt;, &lt;code&gt;ErrConflict&lt;/code&gt; and &lt;code&gt;ErrUnavailable&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/httpapi&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Router, middleware, error envelope, document, &lt;code&gt;/_auth&lt;/code&gt; and &lt;code&gt;/_admin&lt;/code&gt; handlers, CORS&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/jsonnum&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Normalizes JSON numbers to int64 or float64&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;internal/mongodb&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Everything MongoDB-specific: connection, validator translation, provisioner (including realm system databases), &lt;code&gt;Repository&lt;/code&gt; and &lt;code&gt;auth.Store&lt;/code&gt; implementations&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;No MongoDB driver types cross the &lt;code&gt;storage&lt;/code&gt; interface. Another document store (for example Amazon DocumentDB) can be added as a new implementation without touching handlers or validation.&lt;/p&gt;</description></item><item><title>API contract</title><link>https://fernandezvara.github.io/backd/docs/development/contract/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://fernandezvara.github.io/backd/docs/development/contract/</guid><description>&lt;p&gt;&lt;code&gt;api/openapi.yaml&lt;/code&gt; is the contract of the HTTP API, in OpenAPI 3.1. Clients, such as the &lt;a href="../../clients/js/"&gt;JavaScript client&lt;/a&gt;, are written against it, so the server must never drift from it.&lt;/p&gt;
&lt;h2 id="what-it-covers"&gt;What it covers&lt;a class="hd-anchor" href="#what-it-covers" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Every route: health probes, &lt;code&gt;/_auth&lt;/code&gt;, &lt;code&gt;/_admin&lt;/code&gt; and the generic document routes.&lt;/li&gt;
&lt;li&gt;Every status an operation documents explicitly, plus a &lt;code&gt;default&lt;/code&gt; for errors that can happen anywhere (&lt;code&gt;404&lt;/code&gt; for an unknown realm, &lt;code&gt;413&lt;/code&gt;, &lt;code&gt;415&lt;/code&gt;, &lt;code&gt;500&lt;/code&gt;, &lt;code&gt;503&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Response bodies, including the error envelope and its closed list of &lt;code&gt;code&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;Response headers clients rely on: &lt;code&gt;ETag&lt;/code&gt;, &lt;code&gt;Location&lt;/code&gt;, &lt;code&gt;Retry-After&lt;/code&gt;, &lt;code&gt;WWW-Authenticate&lt;/code&gt;, &lt;code&gt;X-Request-ID&lt;/code&gt;, and &lt;code&gt;Cache-Control: no-store&lt;/code&gt; on account data.&lt;/li&gt;
&lt;li&gt;Documents as open objects. Their fields depend on each collection&amp;rsquo;s &lt;code&gt;schema.json&lt;/code&gt;, but &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;_meta&lt;/code&gt; are described exactly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="how-its-enforced"&gt;How it&amp;rsquo;s enforced&lt;a class="hd-anchor" href="#how-its-enforced" aria-hidden="true"&gt;#&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;Two checks keep the spec honest, and both run in CI.&lt;/p&gt;</description></item></channel></rss>