<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tutorial on backd</title><link>https://fernandezvara.github.io/backd/docs/tutorial/</link><description>Recent content in Tutorial on backd</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://fernandezvara.github.io/backd/docs/tutorial/index.xml" rel="self" type="application/rss+xml"/><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>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>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>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>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></channel></rss>