21. Authors with references
Show who wrote each asset with one reference declared in collection.yaml, a profiles collection keyed by owner, 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.
The gallery shows titles, but not who saved them. The name lives somewhere else: the account (email, password, roles) is private, and the directory of members is business data. The usual fix is one more request per card, or copying the name into every asset and keeping it up to date. A reference is the third way: the asset says where its author is, and a read can bring the author along.
A profile per person
A profile is what other members see of a person. It belongs to its owner (_meta.owner, set by backd) and there is at most one per person, which a unique index on _meta.owner guarantees:
examples/config/shelf/main/profiles/schema.json
{
"$comment": "How a member appears to the others (chapter 21): one profile per user. Assets point at it through the `author` reference, so a list can show who wrote each one without a request per card. The profile belongs to its owner (_meta.owner), which is how the reference finds it.",
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 80, "description": "The name shown next to what the member wrote." },
"avatar": { "type": "string", "maxLength": 300, "description": "An initial, an emoji or a short text shown beside the name." },
"bio": { "type": "string", "maxLength": 300, "description": "One line about the member." }
},
"required": ["name"],
"additionalProperties": false
}[
{ "fields": ["_meta.owner"], "unique": true }
]
Add them as config/shelf/main/profiles/schema.json and indexes.json. Its rules are short: every signed-in member reads every profile, and only the owner changes their own.
examples/config/shelf/main/profiles/collection.yaml
# Chapter 21: a profile is what other members see of a person. The account
# (email, password, roles) is private; a profile holds only what the person
# chooses to show, so it can be read by every member and expanded into lists.
rules:
# Every signed-in member reads every profile; anonymous visitors none, so the
# public gallery shows no authors (the reference expands to null there).
read: user != nil
# A member makes their own profile. The unique index on _meta.owner (see
# indexes.json) keeps it to one: a second create is a 409.
create: user != nil
# Only the owner changes or removes it.
update: user != nil && document._meta.owner == user.id
delete: user != nil && document._meta.owner == user.id
# A profile goes with its person when they are erased: what they wrote stays,
# and its author is an unreadable reference (null) from then on.
on_owner_delete:
action: deleteA second create by the same person is a 409: the index says so, not a rule that has to count.
Declare the reference
An asset’s author is the person who owns it, so the source is _meta.owner and the target is the profile whose own _meta.owner is the same user. Add this at the end of config/shelf/main/assets/collection.yaml:
references:
author:
field: _meta.owner
collection: profiles
key: _meta.owner
backd checks it when it starts: the collection exists, the source is something a reference can read, and key has a unique index (a key that could match two profiles is refused, with the file and the fix). Nothing is stored for the reference, and nothing changes for requests that don’t ask.
Ask for it
A read says which references it wants, and which fields of them:
GET /v1/shelf/main/assets/<id>?expand=author
{
"id": "…", "title": "The backd handbook", "kind": "link",
"_meta": { "owner": "…", "version": 1, … },
"_expanded": {
"author": { "id": "…", "name": "Ana", "avatar": "🦊", "bio": "…", "_meta": { … } }
}
}
Without parentheses the whole profile comes back, as a direct read would return it. A gallery card needs two fields, and asking for two keeps the response small:
GET /v1/shelf/main/assets?expand=author(name,avatar)&limit=9
The app does it in one line, with the builder that checks the form:
import { expand } from 'backd-js'
const page = await assets.list({ where, orderBy, limit: 9, expand: expand(['author', ['name', 'avatar']]) })
page.items[0]._expanded?.author // { id, name, avatar } or null
What it costs
The answer says what the expansion did:
X-Backd-Expand-Queries: 1 one query on profiles, for the whole page
X-Backd-Expand-Docs: 9 nine authors placed
A page of nine cards costs one extra query, not nine. Those numbers don’t change with field selection: choosing name,avatar makes the response smaller, not the lookup cheaper. What makes it expensive is asking for more paths, more levels or a many reference with long arrays; nothing is expanded unless asked, and the server refuses past its limits.
A profile nobody can read
profiles reads need a sign-in. The public gallery of chapter 1 has no signed-in visitor, so there the author is null, and so it is for an author who never made a profile or whose profile was erased. The three look identical on purpose: a reader can’t use the field to find out which of them it was. The card just doesn’t print a name.
Writes are the asset’s own
ETag of an expanded read is still the asset’s version, so If-Match works with it. Expanded responses are never cached or answered 304, and a write never expands: sending back what you read (_expanded included) changes nothing, because _expanded is ignored like _meta.
where and order_by use the asset’s own fields: where={"author.name":"Ana"} is a 400 that says so. To find the assets of one person, filter on _meta.owner.
You should see
- A second profile for the same member is refused (
409). expand=authorreturns the profile in_expanded.author; the asset and itsETagare as before, and the response isprivate, no-cache.- With
author(name)onlyidandnamecome back;X-Backd-Expand-Queriesis1for one asset and for a whole page. - An unknown reference is
400 invalid_expandthat names it;whereonauthor.nameis400 invalid_query. - The gallery cards show by Ana; signed out they show no author.
Read Expanding references for the limits and the client for typed _expanded.