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’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 (backd-server serve --with-worker), in dev mode: it follows config/ and bundles the functions itself | config/ |
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 |
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. - 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 backdsaysconfig 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
| # | 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 |
| 16 | A second factor and magic links | sign-in by link and code, an authenticator app for the admin role, user.mfa in a rule, proofs before sensitive actions |
| 17 | Terms and consents | versioned terms accepted at sign-up and sign-in, an optional consent the digest respects, what happens when the terms change |
| 18 | App configuration | a 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 |
| 19 | Types from the config | TypeScript 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 |
| 20 | Teams as organizations | organizations: 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 |
| 21 | Authors with references | a 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 |
| 22 | Search and a team dashboard | a 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 |
| 23 | A pipeline on a status | triggers: on a collection, a when over the transition, by from the session, ordered steps, chains and _meta.chain, fakeTrigger, backd triggers status |
| 24 | Live pins | realtime: 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 |
| 25 | Private notes | an 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 |
| 26 | Your data | a 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/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 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 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 change that does nothing:
docker compose logs backdsays 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’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.
16. A second factor and magic linksSign 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 consentsTerms 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 configurationA 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 configGenerate 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 organizationsSessions 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 referencesShow 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 dashboardSearch 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 statusLet 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 pinsMake 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 notesEncrypt 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 dataA 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.