Files
summercms/.planning/REQUIREMENTS.md
Jakub Zych 1bdbdf041b docs(12-01): reword the Phase 12-14 scope and requirements
- Phase 12 criteria 1-3 and goal: realtime/channels, owner-only share,
  cover import, re-gated search total (D-03, D-04, D-06, D-19, D-20)
- reservations and anonymous public-token views move to Phase 13
  (API-03, API-07); the Discogs cover-price route to Phase 14 (INTG-01)
- the folded lagoon-validate-min-message todo moves to done
2026-10-02 11:44:52 +02:00

22 KiB

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)

  • 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
  • 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
  • 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
  • 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
  • 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)
  • 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
  • KERN-07: Per-request state (user, organization, active collection, locale) travels in context.Context; no package-level globals hold request state
  • KERN-08: A service registry (backpack) lets a plugin publish a service (job manager, authorizer registry, credential resolver) that other plugins resolve by interface
  • KERN-09: A dev watch loop rebuilds and restarts the binary on source change

Console and scaffolding (CLI)

  • 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
  • CLI-02: Scaffolding commands generate a plugin, model, migration, command, job and admin controller with stubs that compile
  • 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)

  • 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
  • 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
  • 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)

  • DATA-01: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener
  • 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
  • DATA-03: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction
  • 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)
  • 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
  • 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
  • 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
  • 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
  • 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)
  • DATA-10: Paginated responses use the exact {data, meta{current_page, last_page, per_page, total}} envelope without a links key
  • 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)

  • 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
  • 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
  • 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)
  • 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
  • HTTP-05: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor
  • 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
  • HTTP-07: A guarded outbound fetch helper enforces host allow-lists, byte caps and timeouts for user-supplied URLs (manual cover URL, Discogs cover)
  • HTTP-08: OpenAPI is generated from swaggo/swag annotations on handlers and openapi-typescript produces the admin SPA's types
  • HTTP-09: CORS and JSON body size limits match the PHP deployment

Authentication and users (AUTH)

  • 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
  • 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
  • AUTH-03: Personal API tokens with a read|write|ai scope ceiling, token CRUD endpoints, and a scope-checking middleware
  • AUTH-04: The must-change-password flag locks the authenticated surface with 423 except the locale and password-change routes
  • 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
  • AUTH-06: OAuth routes are form-urlencoded, CSRF-free, rate limited, and return unwrapped RFC 6749 bodies with the PHP cache headers
  • 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
  • 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)

  • 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
  • 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)

  • 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)

  • 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
  • ADMIN-02: columns.yaml is parsed into a JSON list schema with searchable, sortable, relation columns and datetime/switch renderers
  • ADMIN-03: A relation-manager schema (search, link, unlink, manage/view lists) replaces the one partial field in Collections' editors tab
  • ADMIN-04: Admin CRUD endpoints per controller expose extension hooks (listExtendQuery, formExtendQuery, formBeforeCreate, formBeforeUpdate, relationExtendManageQuery), and bulk delete runs each record's lifecycle hooks
  • ADMIN-05: A settings model binds to a settings screen through the same schema pipeline (search_use_typesense)
  • 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
  • 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)

  • 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
  • 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)
  • 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
  • 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)

  • 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
  • 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)
  • 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
  • DOCS-06: A "Coming from WinterCMS" page maps Winter concepts to SummerCMS equivalents, and every SummerCMS identifier on it is checker-verified
  • 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
  • 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 Pending
API-02 Phase 12 Pending
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)