# Requirements: SummerCMS (Go) **Defined:** 2026-09-16 **Core Value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. ## v1 Requirements Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" below is the plugin author or operator unless a requirement names the Nuxt app, the MCP server or an admin. ### Kernel and plugins (KERN) - [x] **KERN-01**: Application boots from layered YAML config via koanf (base files, env overlay directory, per-plugin namespace, environment variables) with dot-path access and typed section loading - [x] **KERN-02**: A plugin implements one required interface (ID, Requires, Register, Boot); Register runs for all plugins before any Boot, and order follows a topological sort of Requires - [x] **KERN-03**: Plugins opt into capabilities through small optional interfaces (models, migrations, routes, middleware, commands, jobs, listeners, admin controllers, navigation, permissions, schedule, mail templates, config, lang) discovered by type assertion - [x] **KERN-04**: Plugins are Go modules in a go.work workspace that self-register in init(); `summer build` regenerates the blank-import list and rebuilds the binary; `summer plugin:add` wires a new module in - [x] **KERN-05**: A plugin can check at boot whether an optional plugin is registered and skip its integration without a hard import (replaces PHP class_exists guards) - [x] **KERN-06**: A typed event bus offers three primitives: fire-and-forget, fire-and-collect (listener return values merged into one payload), fire-until-handled; plugins listen on other plugins' events - [x] **KERN-07**: Per-request state (user, organization, active collection, locale) travels in context.Context; no package-level globals hold request state - [x] **KERN-08**: A service registry (backpack) lets a plugin publish a service (job manager, authorizer registry, credential resolver) that other plugins resolve by interface - [x] **KERN-09**: A dev watch loop rebuilds and restarts the binary on source change ### Console and scaffolding (CLI) - [x] **CLI-01**: The `summer` binary (cobra) discovers commands registered by plugins and offers rich output (spinner, progress bar, table, prompts) with non-TTY degradation, following the summer-bonfire design - [x] **CLI-02**: Scaffolding commands generate a plugin, model, migration, command, job and admin controller with stubs that compile - [x] **CLI-03**: Migration commands run up, status, and roll back the last migration of a named plugin - [ ] **CLI-04**: Plugins register recurring commands (daily or interval) that a scheduler runs in-process or via `summer schedule:run` - [ ] **CLI-05**: Płytarium commands are ported: oauth-client (create, list without reprinting secrets, add redirect URIs, set scope ceiling), prune-notifications, reindex with drop-old-index flag - [ ] **CLI-06**: Queue worker runs in-process or via `summer queue:work` ### Internationalization and mail (I18N) - [x] **I18N-01**: Translation keys use `vendor.plugin::group.key`, load from per-plugin per-locale YAML files, support parameters and CLDR plurals (go-i18n) for pl and en - [x] **I18N-02**: Locale is resolved per request from the user's persisted preferred_locale with header fallback, and the locale endpoints stay reachable while the must-change-password lock is active - [x] **I18N-03**: Plugins register mail templates and layouts by dotted name with the per-locale suffix convention, rendered with html/template and sent through a driver interface (SMTP via go-mail) ### Data layer (DATA) - [x] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener - [x] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source - [x] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction - [x] **DATA-04**: Relations cover belongsTo, hasOne, hasMany and belongsToMany with ordered results and dedicated pivot models carrying business columns (CollectionEditor role/granted_at/granted_by, album_artists sort_order) - [x] **DATA-05**: Model rule strings (required, between, unique:table, nullable, integer, in) are validated on save via go-playground/validator with translated messages and a 422 error map shaped like Laravel's - [x] **DATA-06**: Mass assignment goes through per-endpoint request DTOs honoring each model's fillable allow-list; serialization honors a hidden deny-list with an explicit per-call override - [x] **DATA-07**: Custom casts exist for jsonable columns, money as a fixed four-decimal string, and encrypted-at-rest secrets (AES-GCM, app-key derived) that are also hidden from serialization - [x] **DATA-08**: A polymorphic file attachment table (owner type, owner id, field, disk path, sort order, public/private) backs attachOne and attachMany, stored via gocloud.dev/blob with the same public URL shape - [x] **DATA-09**: All 25 Płytarium models and their squashed migration set are ported with matching table names, columns, indexes and defaults (migration count is not itself an acceptance number — squashed per plan-time decision D-01 in 05-CONTEXT.md) - [x] **DATA-10**: Paginated responses use the exact `{data, meta{current_page, last_page, per_page, total}}` envelope without a links key - [x] **DATA-11**: Other plugins can hook a model's lifecycle through the GORM callback registry and extend its schema with a companion migration ### HTTP and routing (HTTP) - [x] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources - [x] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler - [x] **HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection) - [x] **HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's five named buckets and inline throttles 1:1 - [x] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor - [x] **HTTP-06**: Response conventions are preserved: empty arrays serialize as [], timestamps as +00:00, tri-state booleans keep null, conditional keys are omitted not nulled, and no blanket envelope or error middleware wraps OAuth routes - [x] **HTTP-07**: A guarded outbound fetch helper enforces host allow-lists, byte caps and timeouts for user-supplied URLs (manual cover URL, Discogs cover) - [x] **HTTP-08**: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types - [x] **HTTP-09**: CORS and JSON body size limits match the PHP deployment ### Authentication and users (AUTH) - [x] **AUTH-01**: User plugin port: registration, login, logout, password reset, email verification, and JWT issue/refresh (golang-jwt) with the same claims and cookie behavior the Nuxt app expects - [x] **AUTH-02**: Organizations with roles; organization fields appear on the user payload through a fire-and-collect event so the fonoteka plugin extends the user plugin without editing it - [x] **AUTH-03**: Personal API tokens with a read|write|ai scope ceiling, token CRUD endpoints, and a scope-checking middleware - [x] **AUTH-04**: The must-change-password flag locks the authenticated surface with 423 except the locale and password-change routes - [x] **AUTH-05**: Direct standard-library OAuth2.1-style authorization server (`wristband`): RFC 8414 metadata, authorize with S256 PKCE and consent screen, authorization_code and rotating refresh_token grants, RFC 7591 dynamic registration, RFC 8707 resource handling, exact backend Basic invalid-client challenge, unchanged backend personal-token 401, and unchanged fonoteka-mcp-owned RFC 9728 protected-resource metadata/Bearer challenge - [x] **AUTH-06**: OAuth routes are form-urlencoded, CSRF-free, rate limited, and return unwrapped RFC 6749 bodies with the PHP cache headers - [x] **AUTH-07**: Connected apps can be listed and revoked; OAuthClient, OAuthAuthCode and OAuthRefreshToken models are ported; fonoteka-mcp completes its install and auth flow unchanged - [x] **AUTH-08**: Backend admin users with roles and a permissions registry are separate from frontend users, and gate both navigation and admin controller access ### Płytarium API (API) - [x] **API-01**: Collections: CRUD, active-context switch (me/context flags plus the opaque channel name from realtime/channels), editor invitations and acceptance, owner-only share link show/update/regenerate - [x] **API-02**: Albums: CRUD, ratings, photo upload and manual cover URL, Discogs cover import (cover_urls), artists/genres/styles lookups, search that treats Typesense as a pre-filter with both the items and the total re-gated in SQL - [ ] **API-03**: Wishlist: items, subscriptions, public-wishlist/{token} views, album reservations (reserve/reveal), purchase and digest triggers - [ ] **API-04**: Notifications: list, mark read, prune; realtime token endpoint owned by the websockets plugin - [ ] **API-05**: CSV import as a multi-step session (store, show/poll, mapping patch, per-row edit, commit, cancel) and CSV export on both authenticated groups - [ ] **API-06**: Per-user and per-org Discogs and AI credentials CRUD with encrypted storage, org-lock flag, and env-to-org-to-user resolution - [ ] **API-07**: Onboarding, public and invitation inspection routes, including the anonymous collection public-token views (public/{token}, its albums and album detail), with their public rate-limit buckets - [ ] **API-08**: Feedback submissions and sitemap output from the stack plugins - [ ] **API-09**: All 154 routes are registered on the correct groups with identical paths, methods, status codes and bodies ### Background jobs (JOBS) - [ ] **JOBS-01**: River runs on the shared *sql.DB with riverdatabasesql for transactional enqueue and riverpgxv5 for LISTEN/NOTIFY; job outcomes (complete, fail, skip) are recorded and queryable through a job manager service - [ ] **JOBS-02**: CSV import write job and Discogs match job (240 s timeout) are ported; the match job re-enqueues itself with a delay on a Discogs rate-limit error instead of failing the batch - [ ] **JOBS-03**: Wishlist digest coalesces notifications in a 30-minute window and deletes its queue row on completion ### Realtime (RT) - [ ] **RT-01**: The websockets plugin publishes to the existing Centrifugo server (gocent) and issues connection and subscription JWTs with the same secret and claims, served at GET /api/realtime/token. *Note (D-12, D-16): the Centrifugo client is a hand-rolled `net/http` client in the framework package `lighthouse/centrifugo` rather than the originally named gocent library, and the publisher lives in that framework package rather than an app websockets plugin.* - [ ] **RT-02**: A channel-namespace authorizer registry lets plugins register authorizers (collection, wishlist); the server re-validates on every subscribe and channel names never expose raw ids - [ ] **RT-03**: A broadcastable model interface emits live patches with bulk-write suppression, separate from durable notifications ### Search (SRCH) - [x] **SRCH-01**: Album documents sync to Typesense scoped by collection_id, behind a settings kill-switch, degrading gracefully when the DB or config is absent - [ ] **SRCH-02**: The reindex command asserts zero documents with collection_id 0 before and after, and can drop the legacy index ### Integrations (INTG) - [ ] **INTG-01**: Discogs client with proactive rate threshold, bounded in-request wait budget, retry-after fallback, and host-locked cover fetch, plus the albums/{id}/cover-price/discogs route - [ ] **INTG-02**: AI cover recognition through provider adapters (Anthropic Go SDK, OpenAI-compatible) with per-credential model and base URL overrides ### Admin (ADMIN) - [x] **ADMIN-01**: fields.yaml is parsed (goccy/go-yaml) into a JSON form schema with text, textarea, checkbox, switch, dropdown (model-method options), relation (nameFrom, emptyOption), plus span, tabs, context and attributes - [x] **ADMIN-02**: columns.yaml is parsed into a JSON list schema with searchable, sortable, relation columns and datetime/switch renderers - [x] **ADMIN-03**: A relation-manager schema (search, link, unlink, manage/view lists) replaces the one `partial` field in Collections' editors tab - [x] **ADMIN-04**: Admin CRUD endpoints per controller expose extension hooks (listExtendQuery, formExtendQuery, formBeforeCreate, formBeforeUpdate, relationExtendManageQuery), and bulk delete runs each record's lifecycle hooks - [x] **ADMIN-05**: A settings model binds to a settings screen through the same schema pipeline (search_use_typesense) - [x] **ADMIN-06**: A minimal Vue 3 + TypeScript SPA renders login, permission-gated navigation, lists, forms and the relation manager for Albums, Artists, Collections, Genres and Styles using generated types - [x] **ADMIN-07**: A plugin extends the compiled admin SPA without a Node rebuild: controller-declared JS/CSS is served from the plugin's embedded files under `{backend.uri}/assets/` and loaded when that controller opens (CSP `script-src 'self'`); `type: widget` fields mount plugin custom elements whose actions the SPA posts with the admin cookie and CSRF header, patching only the declared `fill` fields; `type: partial` form fields and a `config_list.yaml` `headerPartial` render server-side with `html/template` from a controller view model and display without any raw-HTML sink; and controllers register named toolbar actions. Unknown YAML keys, missing templates and unregistered actions fail boot. ### Quality and cutover (QA) - [x] **QA-01**: A fixture recorder captures requests and responses from the running PHP backend for every route plus the Nuxt app's and MCP server's real flows - [x] **QA-02**: A replay-and-diff harness runs fixtures against the Go backend with a normalizer for nondeterministic fields and assertions for the parity classes (nil vs [], date formats, tri-state booleans, envelopes, conditional keys) - [x] **QA-03**: go vet and go test ./... are green at every commit; each phase ends with a unit-test plan; integration tests use testcontainers Postgres - [x] **QA-04**: The first vertical slice (GET /_fonoteka/api/v1/genres) passes the parity diff end to end before further kernel abstraction - [ ] **QA-05**: Cutover: the parity harness is green on all 154 routes and vue-fonoteka-app and fonoteka-mcp run unchanged against the Go backend ### Documentation (DOCS) - [x] **DOCS-01**: `docs/` holds Markdown pages with strict YAML frontmatter (title, description, section, order), grouped into the Winter-mirroring sections, and every framework module under `modules/` is reachable from the sidebar through an API reference page ingested from its README - [x] **DOCS-02**: `summer docs:build` writes a self-contained static site (sidebar, on-page TOC, prev/next, edit-this-page link, client-side search, light/dark/system theme) and `summer docs:serve` previews it on loopback; no Node toolchain, and the only new dependency is `alecthomas/chroma/v2` for syntax highlighting (approved at the 11.1 plan-count checkpoint) - [x] **DOCS-03**: The build emits `llms.txt`, `llms-full.txt` and a clean `.md` beside every `.html` page, and a test asserts all three match the page tree - [ ] **DOCS-04**: Every Go fence in a `docs/` page references compiled source by `src=` (Go fences in ingested module READMEs are identifier-checked only, per D-18); a test fails on a missing or drifted snippet, and referenced Examples carry `// Output:` and run under `go test ./...` - [ ] **DOCS-05**: `go test ./...` runs checkers that fail on stale identifiers (docs pages and module READMEs), broken internal links and anchors, unknown `summer`/runtime CLI command names and forbidden consuming-application names - [x] **DOCS-06**: A "Coming from WinterCMS" page maps Winter concepts to SummerCMS equivalents, and every SummerCMS identifier on it is checker-verified - [x] **DOCS-07**: An `acme/blog` porting walkthrough covers models, migrations, routes, an admin controller and a console command; its code is a real in-root package under `docs/examples/blog`, verified by DOCS-04 - [x] **DOCS-08**: CLAUDE.md's Documentation section records that API, config or CLI changes update both the module README and the affected docs pages, and names the automated checkers ## v2 Requirements Deferred to a later milestone. Tracked but not in the current roadmap. ### Framework - **FW-01**: Generalized three-tier credential resolver (env, org, user with org lock) as one framework piece instead of two copies - **FW-02**: Policy-based row and field-level admin permissions - **FW-03**: Server-rendered theme engine with components in templates (keios.eu port) - **FW-04**: Payments plugins (keios.eu port) - **FW-05**: Sortable, NestedTree, Revisionable, Sluggable behaviors, built on demand - **FW-06**: WASM sandboxed extension API (see seed) ## Out of Scope | Feature | Reason | |---------|--------| | Runtime plugin loading (stdlib plugin, yaegi, RPC) | Rejected in why-go-not-scala.md; compiled plugins only | | MySQL or SQLite support | Postgres only; River and one migration target | | Backend AJAX partial-refresh framework | Admin is a JSON-API SPA; bulk actions become REST endpoints | | Generic response envelope or blanket error middleware | Breaks the three envelope families the PHP API has, including unwrapped OAuth responses | | Improving API response shapes during the port | Parity is the acceptance test | | Native websocket server replacing Centrifugo | Nuxt client connects to Centrifugo directly and must stay unchanged | | Chat, forum, video | wavepath.org territory | | Porting Scala module code | Designs carry over, code does not | ## Traceability Which phases cover which requirements. Updated during roadmap creation. | Requirement | Phase | Status | |-------------|-------|--------| | KERN-01 | Phase 1 | Complete | | KERN-02 | Phase 1 | Complete | | KERN-03 | Phase 1 | Complete | | KERN-04 | Phase 1 | Complete | | KERN-05 | Phase 1 | Complete | | KERN-06 | Phase 1 | Complete | | KERN-07 | Phase 1 | Complete | | KERN-08 | Phase 1 | Complete | | KERN-09 | Phase 1 | Complete | | CLI-01 | Phase 1 | Complete | | CLI-02 | Phase 4 | Complete | | CLI-03 | Phase 5 | Complete | | CLI-04 | Phase 11 | Gaps Found | | CLI-05 | Phase 14 | Pending | | CLI-06 | Phase 11 | Gaps Found | | I18N-01 | Phase 4 | Complete | | I18N-02 | Phase 7 | Complete | | I18N-03 | Phase 4 | Complete | | DATA-01 | Phase 3 | Complete | | DATA-02 | Phase 3 | Complete | | DATA-03 | Phase 5 | Complete | | DATA-04 | Phase 5 | Complete | | DATA-05 | Phase 5 | Complete | | DATA-06 | Phase 5 | Complete | | DATA-07 | Phase 5 | Complete | | DATA-08 | Phase 5 | Complete | | DATA-09 | Phase 5 | Complete | | DATA-10 | Phase 5 | Complete | | DATA-11 | Phase 5 | Complete | | HTTP-01 | Phase 3 | Complete | | HTTP-02 | Phase 3 | Complete | | HTTP-03 | Phase 6 | Complete | | HTTP-04 | Phase 6 | Complete | | HTTP-05 | Phase 6 | Complete | | HTTP-06 | Phase 6 | Complete | | HTTP-07 | Phase 6 | Complete | | HTTP-08 | Phase 6 | Complete | | HTTP-09 | Phase 6 | Complete | | AUTH-01 | Phase 7 | Complete | | AUTH-02 | Phase 7 | Complete | | AUTH-03 | Phase 7 | Complete | | AUTH-04 | Phase 7 | Complete | | AUTH-05 | Phase 8 | Complete | | AUTH-06 | Phase 8 | Complete | | AUTH-07 | Phase 8 | Complete | | AUTH-08 | Phase 9 | Complete | | API-01 | Phase 12 | Complete | | API-02 | Phase 12 | Complete | | API-03 | Phase 13 | Pending | | API-04 | Phase 13 | Pending | | API-05 | Phase 13 | Pending | | API-06 | Phase 13 | Pending | | API-07 | Phase 13 | Pending | | API-08 | Phase 14 | Pending | | API-09 | Phase 15 | Pending | | JOBS-01 | Phase 11 | Gaps Found | | JOBS-02 | Phase 14 | Pending | | JOBS-03 | Phase 14 | Pending | | RT-01 | Phase 11 | Gaps Found | | RT-02 | Phase 11 | Gaps Found | | RT-03 | Phase 11 | Gaps Found | | SRCH-01 | Phase 11 | Complete | | SRCH-02 | Phase 14 | Pending | | INTG-01 | Phase 14 | Pending | | INTG-02 | Phase 14 | Pending | | ADMIN-01 | Phase 9 | Complete | | ADMIN-02 | Phase 9 | Complete | | ADMIN-03 | Phase 9 | Complete | | ADMIN-04 | Phase 9 | Complete | | ADMIN-05 | Phase 9 | Complete | | ADMIN-06 | Phase 10 | Complete | | ADMIN-07 | Phase 10.1 | Complete | | QA-01 | Phase 2 | Complete | | QA-02 | Phase 2 | Complete | | QA-03 | Phase 2 | Complete | | QA-04 | Phase 3 | Complete | | QA-05 | Phase 15 | Pending | | DOCS-01 | Phase 11.1 | Complete | | DOCS-02 | Phase 11.1 | Complete | | DOCS-03 | Phase 11.1 | Complete | | DOCS-04 | Phase 11.1 | Gaps Found | | DOCS-05 | Phase 11.1 | Gaps Found | | DOCS-06 | Phase 11.1 | Complete | | DOCS-07 | Phase 11.1 | Complete | | DOCS-08 | Phase 11.1 | Complete | **Coverage:** - v1 requirements: 85 total - Mapped to phases: 85 - Unmapped: 0 ✓ --- *Requirements defined: 2026-09-16* *Last updated: 2026-09-30 after adding DOCS-01..08 (Phase 11.1 documentation); previously 2026-09-28 after adding ADMIN-07 (Phase 10.1 runtime admin extension point); previously 2026-09-16 after roadmap revision (15 phases, split former Phase 13 into Phase 11 jobs/realtime/search infrastructure and Phase 14 domain jobs/integrations, reordered before the API phases; 100% coverage)*