Skip to content

Tutorial

Build Shelf, a team asset library, one feature per chapter — every chapter ends in a working app.

This tutorial builds Shelf, a team asset library: members save links and notes, curators publish them, public links share them, a digest emails the news. Each chapter adds one backd feature because the app needs it, and ends in a working state with a checklist of what now works.

Short on time? The Quickstart runs one realm and a few curl calls on the backd template project stack. The Tutorial uses its own stack, downloaded from this site, so nothing from the Quickstart carries over.

You do not need a backd checkout: the app, the client and the released containers all download from this site. You need only Docker and a text editor.

Words used here

The short list for this tutorial; the glossary covers the whole documentation.

  • A realm is one application’s world — its users, roles, sessions and settings (shelf). A database groups collections inside it (main), and a collection holds documents (assets). The URLs say it: /v1/{realm}/{database}/{collection}.
  • A function is a small TypeScript program backd runs in a sandbox when something calls it: a person, another function, a clock or the internet.
  • A rule is an expression in the rules: of a collection’s collection.yaml that decides whether a read or write is allowed. A HOLE comment (chapter 3) marks the place where a rule can’t say what you mean and a function has to take over.
  • An operator is the administrator of the realm; a curator is a member who may publish; a member is everyone else.

What you need

  • Docker with Compose v2 (docker compose version prints a version) and free ports 8080 (the app and the API) and, from chapter 13, 9000 (the files’ storage); the first start downloads about 1 GB of images.
  • A text editor and a terminal with a POSIX shell (macOS, Linux, or WSL on Windows — the examples use curl, sed and $VARIABLES). Node 20 or newer only for chapter 11’s feed script (optional).
  • This tutorial’s stack is a separate thing from the backd documentation you are reading: the docs may be at https://localhost:8443 or on GitHub Pages, the tutorial’s app is always http://localhost:8080.

To start over at any point: docker compose down -v (deletes the database), then up -d again.

What runs, and what you edit

docker compose up -d starts these containers (and, from chapter 13, MinIO). You only ever edit files on your side of them:

ContainerWhat it doesYou touch
backdthe API: documents, sign-in, rules, functions’ entry point, jobs and schedules (backd-server serve --with-worker), in dev mode: it follows config/ and bundles the functions itselfconfig/
mongoMongoDB, a one-node replica setnothing — its data lives in a volume (down -v deletes it)
executorruns each function call in its own Deno processnothing
egressthe only way out for functions that declare network: (chapter 6)nothing
nginxserves the app at / and the client at /example/client/, proxies /v1 to backd — one origin for bothapp/ if you change the app
minioS3-compatible storage for files (chapter 13): the browser sends big files straight to it and follows download links to it, on localhost:9000; a one-shot minio-setup creates its bucketnothing

Conventions

  • <angle brackets> in a command are values you substitute. $TOKEN is a session token from POST /_auth/login (chapter 3 shows how); $OP is the operator’s (chapter 4).
  • A config/ path is relative to your starter folder: config/shelf/main/assets/schema.json is the assets collection of the main database in the shelf realm.
  • The stack runs backd in dev mode: save a file under config/ (rules, realm.yaml, schemas, function code) and backd reloads in a moment, bundling a changed function itself. docker compose logs backd says config reloaded, or what is wrong with the file you saved; the previous config keeps serving until it loads.

The starter

Create a folder and fetch the stack and the app:

mkdir shelf && cd shelf
mkdir -p app client config/shelf

# The stack: released backd images + MongoDB + executor + egress + nginx.
curl -fsSLO https://fernandezvara.github.io/backd/tutorial/compose.yaml
curl -fsSLO https://fernandezvara.github.io/backd/tutorial/nginx.conf

# The finished Shelf app: each chapter makes one more part of it work.
for f in index.html app.js style.css; do
  curl -fsSL "https://fernandezvara.github.io/backd/tutorial/app/$f" -o "app/$f"
done
mkdir app/lib
for f in pico.min.css alpine.min.js zip.js; do
  curl -fsSL "https://fernandezvara.github.io/backd/tutorial/app/lib/$f" -o "app/lib/$f"
done

# The JavaScript client the app imports (backd-js, as source).
for f in account admin auth buckets cache client config consents data errors expand files functions index oauth orgs push realtime sha256 storage typed xhr; do
  curl -fsSL "https://fernandezvara.github.io/backd/tutorial/client/$f.js" -o "client/$f.js"
done

Once chapter 1 has written the first config, docker compose up -d runs backd at http://localhost:8080: nginx serves app/ at /, proxies /v1 to the API and serves client/ at /example/client/, exactly like the app’s importmap expects. config/ is where you write the realm — chapter by chapter.

The chapters

#ChapterYou add
1An empty shelfthe shelf realm, the assets and shares collections, dates, indexes, a public gallery with filters and cursor paging
2Memberssign-up, sessions, roles, the account page
3Who may touch whatrules: in collection.yaml, ownership, optimistic concurrency
4Invitationsclosing sign-up; one-time invitation links
5The first functionpublish, admin: true, idempotency, share links
6Reaching the outsidepreview and the network: allowlist
7Functions calling functionsnotify as an internal function, ctx.call; optional: a schema change
8Work that takes timedigest as an async job, retry:
9On a schedulecron schedules, cleanup
10Email for realdelivery function, templates, the Mailbox
11Being called by the internetthe import webhook
12Running itAPI keys, audit feed, the hardening checklist
13Files on assetsa MinIO storage, files: in collection.yaml, uploads, previews, download buttons
14Big files and thumbnailsdirect uploads with a progress bar, attachments, a thumbnail declared as a version, made by a worker
15Sharing and counting downloadsa download function, share links that carry files, the storage in the Admin view
16A second factor and magic linkssign-in by link and code, an authenticator app for the admin role, user.mfa in a rule, proofs before sensitive actions
17Terms and consentsversioned terms accepted at sign-up and sign-in, an optional consent the digest respects, what happens when the terms change
18App configurationa maintenance banner and an export switch declared in _realm/config/, changed by cron functions and administrators, read with a client cache, enforced in an invoke rule
19Types from the configTypeScript types for the app (createClient<Realm>()) and the functions (defineFunction) generated by backd types, following schema changes in dev mode, a stale-types warning, --check and --diff in CI
20Teams as organizationsorganizations: in realm.yaml, a collection that belongs to teams, invitations as a link and by email, roles inside a team, a plan claim read by the pin function, require_mfa, the last-owner rule and deleting a team with its report
21Authors with referencesa profiles collection keyed by the user’s id (custom_id: user), an author reference from _meta.owner, expand=author(name,avatar) on the gallery, the cost headers, null for what a reader can’t see
22Search and a team dashboarda weighted text index on assets, $search sorted by relevance in the user’s language, an aggregate: on pins with its missing-index warning and fix, a team time zone claim and pins per member per month
23A pipeline on a statustriggers: on a collection, a when over the transition, by from the session, ordered steps, chains and _meta.chain, fakeTrigger, backd triggers status
24Live pinsrealtime: true on pins, a subscription of the session’s team with subscribe(), a list that follows other members’ pins, what a removed member sees, session_ended, and the realtime_subscription_checks_total metric for grouped versus unfiltered subscriptions
25Private notesan encrypted field on pins with a key you create and save, reveal for the author and the team’s admins (null and _meta.redacted for the rest), the ciphertext in MongoDB, an instant key rotation and an export that opens the notes only with the key
26Your dataa second required consent (the privacy notice), compliance.export, a Download my data button on the account page that asks with a fresh proof, follows the worker and shows the README read from the zip, the email that links the app, and backd user export for support

The three accounts

From chapter 2 on you use three accounts, all created in chapter 2 while sign-up is open: operator@shelf.example (the administrator), curator@shelf.example (publishes) and one of your own (a member). Chapter 4 closes sign-up, so create them before it — the lock-out section there is the way back if you didn’t.

How a chapter reads

Each chapter is a diff on the state the previous one left: the files to create or change (quoted from the same files the repository runs and tests; a fetch block lists them as a download loop when you’d rather not type), the commands to run, and a “you should see” list at the end — if every line holds, the chapter worked. Chapters follow each other: each starts from the state the previous one left, so do them in order (or start from the repository’s examples/config/shelf when you want the finished realm). A chapter that needs something extra says so in its first lines, like chapter 13’s storage, chapter 16’s authenticator app or chapter 11’s Node script.

When something is off

  • curl -s http://localhost:8080/readyz answers once MongoDB is a replica set and backd serves; for a minute after up it may not. docker compose ps shows which container is down.
  • docker compose logs backd is the first place to look. A bad config file doesn’t stop backd in dev mode: the log names the file and line, and the last good config keeps serving; every request is logged with its request_id, which also appears in every error body.
  • An error body is {"error": {"code", "message", "details"}}, and the status says what kind: 401 not signed in or the session ended (log in again; sessions last 30 days); 403 signed in but a rule or an invoke expression refuses it; 404 the thing doesn’t exist or you may not read it; 400 invalid input, with the failing fields in details; 409 a unique index or an already-used value; 412 the document changed since you read it; 503 a function call while the executor is still starting.
  • A change that does nothing: docker compose logs backd says whether it reloaded (config reloaded) or why it refused (a TypeScript error in a function shows Deno’s output, with the file and line). The log also shows the function’s console.log lines and, when a call fails, its error.
  • Start over: docker compose down -v, then up -d — and sign up the three accounts again (chapter 2).
1. An empty shelf

The shelf realm, the assets collection, dates stored as dates, and a public gallery with filters and cursor paging.

2. Members

Turn authentication on: sign-up, sessions, roles, and an account page with password change and account deletion.

3. Who may touch what

rules: in collection.yaml: read, create, update and delete rules per collection — and the hole only a function can close.

4. Invitations

signup: invite closes the realm; operators hand out one-time links — emailed once chapter 10 configures delivery.

5. The first function

publish, share and share-open: what rules can’t say, TypeScript on a Deno sandbox says — admin access, invoke rules, idempotency.

6. Reaching the outside

preview calls api.microlink.io — functions have no network by default; network: is the whole allowlist.

7. Functions calling functions

notify is internal — no HTTP route, only callers that declare it. ctx.call, calls: and fakeCall tests.

8. Work that takes time

digest as an async job — the call answers immediately, the caller polls, retry covers a transient failure.

9. On a schedule

schedule: cron runs a function with nobody calling — ctx.admin.db always, no user, no input. cleanup sweeps expired shares.

10. Email for real

email-capture delivers in dev (the Mailbox), postmark for real — templates, hosted pages, ctx.email.send, emailed invitations.

11. Being called by the internet

the import webhook — mode: webhook, HMAC signatures over the raw body, dedupe by the sender’s event id.

12. Running it

API keys for machines, the audit trail in the app, and the checklist before any of this faces the internet.

13. Files on assets

Give assets a file: backd’s own storage in a MinIO next to it, files: in collection.yaml, uploads, previews and download buttons, and the rules that already protect them.

14. Big files and thumbnails

Direct uploads with a progress bar, several attachments per asset, and a thumbnail declared as a version that a worker makes.

15. Sharing and counting downloads

A download function that counts and returns the link, share links that open with their files, and the operator’s view of the storage.

16. A second factor and magic links

Sign in with a link by email, require an authenticator app of the admin role, a rule that asks for it, and proofs before sensitive actions.

17. Terms and consents

Terms members accept to have an account, an optional consent for the digest by email, a function that respects it, and what happens when the terms change.

18. App configuration

A maintenance banner every visitor sees, a switch that hides the export button and blocks the export function, changed by cron functions and administrators, cached by the client and pushed to open pages with realtime.

19. Types from the config

Generate TypeScript types from Shelf’s config for the app and the functions, watch them follow a schema change in dev mode, see the stale-types warning, and keep them current in CI.

20. Teams as organizations

Sessions that belong to a team: people sign in to a team by its short name, start one with the operator’s invitation, invite each other with a link or an email, keep a team’s pins to its members, give roles inside a team, limit a team’s pins by plan with a claim, and delete a team with its report first.

21. Authors with references

Show who wrote each asset with one reference declared in collection.yaml, a profiles collection keyed by user id, and expand=author(name,avatar) on the list: the cost headers before and after selecting fields, a profile nobody can read, and writes that keep the asset’s own ETag.

22. Search and a team dashboard

Search Shelf’s assets with a weighted text index, best match first and in the user’s language, and show how many pins each member added per month in the team’s time zone with one declared aggregation: the missing-index warning and its fix.

23. A pipeline on a status

Let members propose assets for the gallery and run the steps that follow, announce, publish and thank, as triggers on the proposal’s status.

24. Live pins

Make a team’s pinboard live: one subscription filtered by team, what a removed member sees, what the stream costs and how to check it with the metrics.

25. Private notes

Encrypt a field before it reaches MongoDB: create and save a key, mark the field, let only the author and the team’s admins read it, look at the ciphertext, rotate the key and export the notes with it.

26. Your data

A privacy notice members accept, and a button that downloads a copy of everything Shelf holds about them: a zip built by the worker, a README that explains it, and an email that links the app.

Edit this page · Last updated 13 Nov 2026