Mark CLI-02 complete, log scaffolding registry and leaf-check decisions, and move Phase 4 progress to 1/4 plans. Co-authored-by: Cursor <cursoragent@cursor.com>
18 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 buildregenerates the blank-import list and rebuilds the binary;summer plugin:addwires 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
summerbinary (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 27 migrations are ported with matching table names, columns, indexes and defaults
- 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 seven 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: OAuth2.1 authorization server on zitadel/oidc: RFC 8414 metadata, authorize with PKCE and consent screen, token endpoint for authorization_code and refresh_token, RFC 7591 dynamic client registration, RFC 8707 resource parameter tolerance, exact WWW-Authenticate and protected-resource-metadata headers
- 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 returns an opaque channel name), editor invitations and acceptance, share link regeneration, public token views
- API-02: Albums: CRUD, ratings, reservations, photo upload and manual cover URL, Discogs cover price, artists/genres/styles lookups, search that treats Typesense as a pre-filter re-gated in SQL
- API-03: Wishlist: items, subscriptions, public-wishlist/{token} views, 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 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
- 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
- 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
partialfield 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
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
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 | Pending |
| CLI-04 | Phase 11 | Pending |
| CLI-05 | Phase 14 | Pending |
| CLI-06 | Phase 11 | Pending |
| I18N-01 | Phase 4 | Pending |
| I18N-02 | Phase 7 | Pending |
| I18N-03 | Phase 4 | Pending |
| DATA-01 | Phase 3 | Complete |
| DATA-02 | Phase 3 | Complete |
| DATA-03 | Phase 5 | Pending |
| DATA-04 | Phase 5 | Pending |
| DATA-05 | Phase 5 | Pending |
| DATA-06 | Phase 5 | Pending |
| DATA-07 | Phase 5 | Pending |
| DATA-08 | Phase 5 | Pending |
| DATA-09 | Phase 5 | Pending |
| DATA-10 | Phase 5 | Pending |
| DATA-11 | Phase 5 | Pending |
| HTTP-01 | Phase 3 | Complete |
| HTTP-02 | Phase 3 | Complete |
| HTTP-03 | Phase 6 | Pending |
| HTTP-04 | Phase 6 | Pending |
| HTTP-05 | Phase 6 | Pending |
| HTTP-06 | Phase 6 | Pending |
| HTTP-07 | Phase 6 | Pending |
| HTTP-08 | Phase 6 | Pending |
| HTTP-09 | Phase 6 | Pending |
| AUTH-01 | Phase 7 | Pending |
| AUTH-02 | Phase 7 | Pending |
| AUTH-03 | Phase 7 | Pending |
| AUTH-04 | Phase 7 | Pending |
| AUTH-05 | Phase 8 | Pending |
| AUTH-06 | Phase 8 | Pending |
| AUTH-07 | Phase 8 | Pending |
| AUTH-08 | Phase 9 | Pending |
| 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 | Pending |
| JOBS-02 | Phase 14 | Pending |
| JOBS-03 | Phase 14 | Pending |
| RT-01 | Phase 11 | Pending |
| RT-02 | Phase 11 | Pending |
| RT-03 | Phase 11 | Pending |
| SRCH-01 | Phase 11 | Pending |
| SRCH-02 | Phase 14 | Pending |
| INTG-01 | Phase 14 | Pending |
| INTG-02 | Phase 14 | Pending |
| ADMIN-01 | Phase 9 | Pending |
| ADMIN-02 | Phase 9 | Pending |
| ADMIN-03 | Phase 9 | Pending |
| ADMIN-04 | Phase 9 | Pending |
| ADMIN-05 | Phase 9 | Pending |
| ADMIN-06 | Phase 10 | Pending |
| 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 |
Coverage:
- v1 requirements: 76 total
- Mapped to phases: 76
- Unmapped: 0 ✓
Requirements defined: 2026-09-16 Last updated: 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)