Design Decisions
Rationale behind YotoShelf's key design choices: auth model, data model, collections, audio pipeline, and more.
This page records the rationale behind YotoShelf's major design decisions. For the technical implementation details, see Architecture.
go-yoto: separate module
The Yoto API client lives in a separate Go module at
gitlab.com/yotoshelf/go-yoto
rather than inside the YotoShelf monorepo. This makes it independently importable by
other Go projects without pulling in YotoShelf's full dependency tree.
During development, the main module references it via a replace directive.
Local users + OIDC (not OIDC-only)
YotoShelf supports both local user accounts (admin-created, argon2id passwords) and optional OIDC login. OIDC-only would require every self-hoster to run a separate identity provider — a significant barrier. Local accounts are the baseline; OIDC is configured when an existing IdP is available. Local users can be migrated to OIDC later via admin account association.
Collections-centric design
Collections are the primary organising concept — not a layer added on top of a flat card library. Every card must be created in a collection. Publishing is collection-aware. "All Cards" is a UI filter, not a real collection.
This matches how people think about their content: "Family cards", "School", "Audiobooks". It also provides a natural access control boundary (collection-level roles) without needing per-card permissions.
SQLite as single source of truth
SQLite is the authoritative store for all UX operations. card.yaml is
a derived export, not a primary input. This avoids sync conflicts between two
authoritative sources and makes the web UI simpler. The filesystem watcher that
imports external card.yaml edits is a power-user escape hatch, not
a core workflow.
Dependencies philosophy
YotoShelf uses well-travelled, battle-tested dependencies freely. Reimplementing established patterns (session management, OAuth2, structured logging) adds audit burden with no benefit. The project rolls its own only where no good option exists.
Auth: gorilla/sessions CookieStore
Session cookies are HMAC-signed (and optionally AES-encrypted). Yoto tokens are explicitly not stored in the session cookie — they are too large and too sensitive. Only the internal user ID is in the cookie; tokens live in SQLite encrypted at rest.
Token encryption and key rotation
Yoto tokens are encrypted at rest using AES with a key derived from an environment variable. Key rotation is a first-class operation: a dedicated admin endpoint re-encrypts all stored tokens with the new key. Losing the key requires re-linking all Yoto accounts; users should store their passphrase as a backup.
Audio pipeline: keep originals, transcode for preview
YotoShelf transcodes audio to AAC 128 Kbps 44.1 kHz locally for preview playback, but uploads the original file to Yoto when publishing. Yoto re-encodes everything server-side regardless — so uploading a lossless source yields one lossy step (Yoto's), not two. Originals are retained on the library volume for re-publish.
128 Kbps VBR is the default transcode quality for local preview. Higher bitrates are not perceptibly better on Yoto Player hardware.
Terminology: cover vs label
Cover image is the digital artwork shown in the Yoto app — 638×1011 px portrait,
uploaded via POST /media/coverImage/user/me/upload. This is Yoto's own term.
Label is the printable sticker output — a PDF or SVG generated by YotoShelf for printing on blank NFC card stock. These are distinct: a card has a cover image and may have a printed label, but the two are not the same thing.
The project adopted Yoto's terminology after verifying it at the API level. Any documentation or code using "cover" for the digital asset and "label" for the printable sticker is correct; uses of "label" to mean the digital cover are legacy and should be updated.
No self-registration
Admin creates user accounts or configures OIDC. There is no self-registration in v1. This matches the intended deployment model: a household or small group managed by one administrator, not a public SaaS.
No anonymous telemetry
YotoShelf does not phone home. Adoption is tracked via container registry download counts only. Prometheus metrics are available but opt-in and token-authenticated.
i18n from day one
Internationalisation is built in from the start using Paraglide JS for the frontend. Translations are community-contributed via Weblate. No paid translation service.
Federation: horizon, not MVP
Instance-to-instance card replication is on the horizon but not in the MVP. The data model — stable card UUIDs, content hashes, collection ownership, standard REST API surface — is designed to support federation without pre-building the replication protocol. Instances are closed by default; sharing requires a bi-directional handshake.
Job queue: SQLite-backed, survives restarts
Background jobs (publish, transcode, import) are persisted in SQLite so they survive container restarts. Per-track retry with order preservation. Graceful shutdown flushes in-progress jobs and closes SQLite cleanly on SIGTERM.
Shareable label links
Share links use the format /share/{slug}/{token} where the token is
an HMAC secret in the path segment (not a query parameter). This allows aggressive
rate limiting at the path level. Default expiry is 7 days, configurable per share.
Users can manage, edit, and revoke shares from the UI, with a break-glass "revoke all" option.