Architecture
Tech stack, auth architecture, data model, and key architectural rationale for YotoShelf.
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
| Concern | Technology | Notes |
|---|---|---|
| HTTP server | Go + chi + huma | chi for routing; huma for OpenAPI generation and type-safe handlers |
| Frontend | Astro + Svelte 5 | Astro static MPA; interactive islands use Svelte 5 |
| Database | SQLite (modernc.org/sqlite) | Pure Go — no CGo, works in distroless |
| Session management | gorilla/sessions CookieStore | HMAC-signed cookies; Yoto tokens stored in SQLite, not cookies |
| Media processing | ffmpeg | Local audio transcoding for preview playback |
| Container | Multi-stage OCI | Node (frontend) → Go (binary) → Alpine (runtime + ffmpeg) |
Auth architecture
YotoShelf has two separate auth concerns:
- 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).
- 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
| Concern | App login | Yoto account link |
|---|---|---|
| Protocol | Local password or OIDC (Auth Code) | OAuth2 Auth Code + PKCE |
| Token storage | Session cookie (user ID only) | SQLite yoto_accounts table (encrypted at rest) |
| Token lifetime | Session = configurable (default 7 days) | Access token = hours; refresh token = long-lived |
| Refresh | Re-auth or OIDC refresh | Background 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
| Table | Purpose |
|---|---|
users | Authenticated users. auth_type: local or oidc. Role: admin | user | viewer. |
yoto_accounts | Linked Yoto accounts per user. Tokens encrypted at rest. Multiple accounts per user supported. |
collections | Primary organising unit. Cards belong to collections. Each collection has member roles: owner, contributor, viewer. |
cards | MYO cards. Each card has a home collection; can be linked to others. Status: draft | ready. |
card_tracks | Audio tracks per card. Includes metadata_embedded boolean. |
publish_records | Per-card, per-Yoto-account publish state. Cascade-deletes with card. |
share_links | Time-limited label share links. HMAC token in path for aggressive rate limiting. |
jobs | SQLite-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
| Package | Purpose |
|---|---|
cmd/ | Executable entry points: serve, gen-openapi, etc. |
internal/api | HTTP handlers, huma registration, middleware |
internal/auth | Session store, local auth, OIDC, rate limiter |
internal/card | Card domain logic, path manager, YAML export |
internal/db | SQLite open, sqlc-generated queries |
internal/config | Environment variable config (CLI flags > env vars > defaults) |
internal/job | SQLite-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:
- Node (frontend):
npm ci && npm run build→frontend/dist/ - Go (binary):
CGO_ENABLED=0 go buildwith embedded frontend - 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).