YotoShelf
Reference

Architecture

Tech stack, auth architecture, data model, and key architectural rationale for YotoShelf.

Source gitlab.com/yotoshelf/yotoshelf Current as of

This page summarises the architectural choices made during YotoShelf's design phase. For design-level decisions and the rationale behind them, see Design Decisions.

Runtime stack

ConcernTechnologyNotes
HTTP serverGo + chi + humachi for routing; huma for OpenAPI generation and type-safe handlers
FrontendAstro + Svelte 5Astro static MPA; interactive islands use Svelte 5
DatabaseSQLite (modernc.org/sqlite)Pure Go — no CGo, works in distroless
Session managementgorilla/sessions CookieStoreHMAC-signed cookies; Yoto tokens stored in SQLite, not cookies
Media processingffmpegLocal audio transcoding for preview playback
ContainerMulti-stage OCINode (frontend) → Go (binary) → Alpine (runtime + ffmpeg)

Auth architecture

YotoShelf has two separate auth concerns:

  1. App authentication — who is logged in to YotoShelf. Supports both local user accounts (admin-created, argon2id passwords) and OIDC (optional, for self-hosters with an existing IdP).
  2. Yoto account linking — OAuth2 PKCE flow to link a Yoto account, storing tokens encrypted in SQLite.

The Go backend is a Backend-for-Frontend (BFF): it handles all auth and serves the static frontend. The frontend never handles tokens directly. Go middleware checks the session cookie and redirects to /login for unauthenticated requests.

Dual OAuth2 flows

ConcernApp loginYoto account link
ProtocolLocal password or OIDC (Auth Code)OAuth2 Auth Code + PKCE
Token storageSession cookie (user ID only)SQLite yoto_accounts table (encrypted at rest)
Token lifetimeSession = configurable (default 7 days)Access token = hours; refresh token = long-lived
RefreshRe-auth or OIDC refreshBackground refresh; Yoto tokens are single-use

Token refresh serialisation

One yoto.Client instance per linked Yoto account, shared across all background workers via a sync.RWMutex-protected map. The client's internal mutex serialises refresh operations. Yoto refresh tokens are single-use — the new token is persisted atomically in a database transaction before the old one is discarded.

Data model overview

SQLite is the single source of truth. card.yaml is a derived export, written after every SQLite change. The web UI never reads directly from card.yaml; a filesystem watcher imports external edits as a power-user escape hatch.

Key tables

TablePurpose
usersAuthenticated users. auth_type: local or oidc. Role: admin | user | viewer.
yoto_accountsLinked Yoto accounts per user. Tokens encrypted at rest. Multiple accounts per user supported.
collectionsPrimary organising unit. Cards belong to collections. Each collection has member roles: owner, contributor, viewer.
cardsMYO cards. Each card has a home collection; can be linked to others. Status: draft | ready.
card_tracksAudio tracks per card. Includes metadata_embedded boolean.
publish_recordsPer-card, per-Yoto-account publish state. Cascade-deletes with card.
share_linksTime-limited label share links. HMAC token in path for aggressive rate limiting.
jobsSQLite-backed job queue. Survives restarts. Per-track retry with order preservation.

Collections model

Collections are the primary organising concept. "All Cards" is a virtual UI filter, not a real collection. Every card must be created in an explicit collection.

A card has one home collection (ownership/deletion anchor) and can be linked to additional collections. Removing from the home collection deletes the card; removing from a linked collection removes the link only.

If all members are removed from a collection, it becomes orphaned. Admins can re-assign or delete orphaned collections via the admin health page.

Go package layout

PackagePurpose
cmd/Executable entry points: serve, gen-openapi, etc.
internal/apiHTTP handlers, huma registration, middleware
internal/authSession store, local auth, OIDC, rate limiter
internal/cardCard domain logic, path manager, YAML export
internal/dbSQLite open, sqlc-generated queries
internal/configEnvironment variable config (CLI flags > env vars > defaults)
internal/jobSQLite-backed job queue and worker pool

Frontend architecture

Astro generates a static multi-page application embedded into the Go binary at build time via //go:embed all:frontend/dist. The Go server serves static files with long-lived cache headers for hashed /_astro/* assets.

Interactive components (card editor, collection browser, publish matrix) are Svelte 5 islands hydrated with client:load. The Tailwind + shadcn-svelte component library provides accessible UI primitives.

In development, Astro's dev server proxies /api/** to the Go server at :8080 via vite.server.proxy.

Container build

Three-stage OCI build:

  1. Node (frontend): npm ci && npm run buildfrontend/dist/
  2. Go (binary): CGO_ENABLED=0 go build with embedded frontend
  3. Alpine (runtime): binary + ffmpeg. Database on a mounted volume.

Observability

GET /metrics exposes Prometheus-format metrics behind token auth. Metrics cover card count, publish rates, storage, job queue depth, and errors. No anonymous telemetry or phone-home — adoption is tracked via container registry downloads only. Structured logging via Go slog (JSON format).