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.
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
- 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’scollection.yamlthat decides whether a read or write is allowed. AHOLEcomment (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 versionprints 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,sedand$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:8443or on GitHub Pages, the tutorial’s app is alwayshttp://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:
| Container | What it does | You touch |
|---|---|---|
backd | the API: documents, sign-in, rules, functions’ entry point, jobs and schedules (serve --with-worker) | config/ (mounted read-only) |
mongo | MongoDB, a one-node replica set | nothing — its data lives in a volume (down -v deletes it) |
executor | runs each function call in its own Deno process | nothing |
egress | the only way out for functions that declare network: (chapter 6) | nothing |
nginx | serves the app at / and the client at /example/client/, proxies /v1 to backd — one origin for both | app/ if you change the app |
minio | S3-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 bucket | nothing |
functions-build | a one-shot job that bundles the functions’ TypeScript before backd starts | run again after editing a function |
Conventions
<angle brackets>in a command are values you substitute.$TOKENis a session token fromPOST /_auth/login(chapter 3 shows how);$OPis the operator’s (chapter 4).- A
config/path is relative to your starter folder:config/shelf/main/assets/schema.jsonis theassetscollection of themaindatabase in theshelfrealm. - Rules,
realm.yamland schemas are read at startup:docker compose restart backdafter changing them. Function code (and itsfunction.yaml) is bundled byfunctions-build: run it, then restart.
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; 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 admin auth client data errors files functions index sha256 storage 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
| # | Chapter | You add |
|---|---|---|
| 1 | An empty shelf | the shelf realm, the assets and shares collections, dates, indexes, a public gallery with filters and cursor paging |
| 2 | Members | sign-up, sessions, roles, the account page |
| 3 | Who may touch what | rules: in collection.yaml, ownership, optimistic concurrency |
| 4 | Invitations | closing sign-up; one-time invitation links |
| 5 | The first function | publish, admin: true, idempotency, share links |
| 6 | Reaching the outside | preview and the network: allowlist |
| 7 | Functions calling functions | notify as an internal function, ctx.call; optional: a schema change |
| 8 | Work that takes time | digest as an async job, retry: |
| 9 | On a schedule | cron schedules, cleanup |
| 10 | Email for real | delivery function, templates, the Mailbox |
| 11 | Being called by the internet | the import webhook |
| 12 | Running it | API keys, audit feed, the hardening checklist |
| 13 | Files on assets | a MinIO storage, files: in collection.yaml, uploads, previews, download buttons |
| 14 | Big files and thumbnails | direct uploads with a progress bar, attachments, a thumbnail declared as a version, made by a worker |
| 15 | Sharing and counting downloads | a download function, share links that carry files, the storage in the Admin view |
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.
When something is off
curl -s http://localhost:8080/readyzanswers once MongoDB is a replica set and backd serves; for a minute afterupit may not.docker compose psshows which container is down.docker compose logs backdis the first place to look. A bad config file fails startup and the log names the file and line; every request is logged with itsrequest_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 aninvokeexpression refuses it; 404 the thing doesn’t exist or you may not read it; 400 invalid input, with the failing fields indetails; 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 function change that does nothing: run
docker compose run --rm functions-build(it prints onebundled …line per function; a TypeScript error stops here) anddocker compose restart backd;docker compose logs backdshows the function’sconsole.loglines and, when a call fails, its error. - Start over:
docker compose down -v, thenup -d— and sign up the three accounts again (chapter 2).
The shelf realm, the assets collection, dates stored as dates, and a public gallery with filters and cursor paging.
2. MembersTurn authentication on: sign-up, sessions, roles, and an account page with password change and account deletion.
3. Who may touch whatrules: in collection.yaml: read, create, update and delete rules per collection — and the hole only a function can close.
4. Invitationssignup: invite closes the realm; operators hand out one-time links — emailed once chapter 10 configures delivery.
5. The first functionpublish, share and share-open: what rules can’t say, TypeScript on a Deno sandbox says — admin access, invoke rules, idempotency.
6. Reaching the outsidepreview calls api.microlink.io — functions have no network by default; network: is the whole allowlist.
notify is internal — no HTTP route, only callers that declare it. ctx.call, calls: and fakeCall tests.
8. Work that takes timedigest as an async job — the call answers immediately, the caller polls, retry covers a transient failure.
9. On a scheduleschedule: cron runs a function with nobody calling — ctx.admin.db always, no user, no input. cleanup sweeps expired shares.
10. Email for realemail-capture delivers in dev (the Mailbox), postmark for real — templates, hosted pages, ctx.email.send, emailed invitations.
11. Being called by the internetthe import webhook — mode: webhook, HMAC signatures over the raw body, dedupe by the sender’s event id.
12. Running itAPI keys for machines, the audit trail in the app, and the checklist before any of this faces the internet.
13. Files on assetsGive 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 thumbnailsDirect uploads with a progress bar, several attachments per asset, and a thumbnail declared as a version that a worker makes.
15. Sharing and counting downloadsA download function that counts and returns the link, share links that open with their files, and the operator’s view of the storage.