# SummerCMS (Go) ## What This Is SummerCMS is a Go rewrite of the WinterCMS/OctoberCMS content management framework for the Golem15 stack. It keeps what makes WinterCMS productive for us (plugins that extend each other, YAML-driven admin forms and lists, models/controllers/components, scaffolding commands, a headless API layer) and drops the parts that do not survive a compiled language (runtime plugin autoloading, PHP-style mutable magic). It is built by Golem15 developers and AI agents, for Golem15 projects that today run on the WinterCMS starter. v1 is a headless backend that runs one real project, Płytarium (`fonoteka`), with its existing Nuxt 4 app and MCP server unchanged. ## 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. ## Requirements ### Validated Validated in Phase 1: Framework kernel foundation - [x] A single `summer` binary boots an application from layered YAML config via koanf (base + env overlays + per-plugin namespaces + environment variables), the layering design carried over from summer-compass (HOCON syntax itself is not used: no maintained Go parser exists) - [x] Plugins are Go modules registered at build time through a generated import list; `summer make:plugin` scaffolds one and `summer build` rebuilds the binary; a dev watch loop rebuilds on change - [x] A typed in-process event bus lets plugins subscribe to each other's events - [x] Console command framework with registration, dispatch and rich output (spinners, progress, tables, prompts), the design carried over from summer-bonfire; `summer make:plugin` / `plugin:add` shipped in Phase 1; model, migration, command, job, and admin-controller scaffolds shipped in Phase 4 Validated in Phase 2: API parity harness bootstrap - [x] `summer parity:record`, `parity:replay` and `parity:proxy` on the Phase 1 bonfire kernel capture live PHP (154 routes plus real Nuxt and MCP flows) into versioned YAML fixtures with secrets scrubbed to placeholders - [x] Replay-and-diff against an arbitrary HTTP backend reports JSON-path and byte-offset mismatches; the normalizer fails the named parity classes (nil vs `[]`, Carbon `Z` vs `+00:00`, tri-state booleans, envelopes, money string vs number) - [x] App `go test ./parity` starts testcontainers Postgres; unported PHP routes stay pending and never count as a Go pass; `scripts/check-phase2.sh --fresh-php` is the sign-off gate Validated in Phase 3: First vertical slice — genres end to end - [x] GORM on Postgres through one shared pgx-stdlib `*sql.DB`; per-plugin gormigrate sets run up and down in `Requires` order with per-plugin history tables; AutoMigrate is not the schema source (River's LISTEN/NOTIFY pool remains a Phase 11 seam) - [x] Plugins register named middleware and `net/http` ServeMux route groups with typed integer params; JWT-guarded `GET /_fonoteka/api/v1/genres` returns seeded Genre rows from Postgres - [x] That genres route passes the Phase 2 PHP fixture replay: corpus 154 recorded, 1 passing, 153 pending (pending never counts as a Go pass); `scripts/check-phase3.sh` is the sign-off gate Validated in Phase 4: CLI scaffolding, i18n and mail - [x] Scaffolding commands `make:plugin`, `make:model`, `make:migration`, `make:command`, `make:job`, and `make:admin-controller` emit compiling, vet-clean Winter-shaped stubs wired through `registry.gen.go` - [x] i18n with CLDR plurals and namespaced keys (`vendor.plugin::group.key`), pl and en, loaded from per-plugin per-locale YAML, the design carried over from summer-phrasebook - [x] Plugins register mail templates and layouts by dotted name with the per-locale suffix convention; html/template + Goldmark render through memory, log, and SMTP (go-mail) drivers; `scripts/check-phase4.sh` is the sign-off gate Validated in Phase 5: Data layer full fidelity - [x] All 25 Płytarium models ported with matching tables, relations, casts, lifecycle hooks, and fillable/hidden/encrypted mass-assignment discipline; squashed gormigrate sets run up and down; schema-diff vs a committed PHP snapshot; `migrate:rollback --plugin=fonoteka` isolates that plugin - [x] `lagoon.Fill` allow-list copy, `lagoon.Validate` Laravel rule strings, `lagoon.Paginate` `{data, meta}` envelope, AES-256-GCM `lagoon.Encrypted`, Jsonable TEXT casts, MoneyString 4-decimal numeric, and `system_files` attach (blob + Winter Thumb names). HTTP public URL serving of uploads remains Phase 6. Validated in Phase 7: User plugin and authentication - [x] User plugin port: registration, login, logout, password reset, email verification, JWT issue/refresh with Nuxt claims, organizations via fire-and-collect, personal API tokens with a read|write|ai ceiling, and the must-change-password 423 lock with locale exemption - [x] Locale resolves per request from preferred_locale then Accept-Language then app.locale, including while the lock is active - [x] `surf.ServeCommand` and `app.Handler` publish the uploads `*blob.Bucket` so assembled avatar POST is 200 Validated in Phase 8: OAuth2.1 authorization server - [x] Direct standard-library OAuth 2.1-style authorization server (`wristband`): RFC 8414 metadata, RFC 7591 dynamic registration, authorize with S256 PKCE and JWT-guarded consent, authorization_code and rotating refresh_token grants with replay lineage-kill, RFC 8707 resource handling, connected-app list/revoke, `fonoteka:oauth-client` operator command, and the `/me` bootstrap the unchanged fonoteka-mcp process uses - [x] Byte parity with recorded PHP across all nine OAuth routes and a 17-step MCP lifecycle; the real unmodified fonoteka-mcp completes discovery, DCR, PKCE, consent, token, tool call, refresh, replay rejection and revoke against the Go binary; `scripts/check-phase8.sh` is the sign-off gate (its Playwright UI matrix stage is a named carried-forward follow-up) ### Active Framework kernel - [ ] Plugins declare models, migrations, routes, console commands, jobs, event listeners, admin controllers and navigation through one plugin descriptor, and can extend other plugins' models through ordinary Go interfaces and events - [ ] YAML `rules:` validation mapped onto go-playground/validator with translated messages Data layer - (moved to Validated in Phase 5) HTTP and auth - (user plugin / JWT / orgs / personal tokens / password lock — moved to Validated in Phase 7) - (OAuth authorization server — moved to Validated in Phase 8; shipped as the direct standard-library `wristband` package rather than zitadel/oidc) - [ ] All 154 Płytarium API routes ported with byte-compatible request and response shapes (collections, albums, artists, genres, styles, ratings, reservations, wishlist, sharing and invitations, notifications, realtime channel auth, CSV import/export, locale, user context, credentials) Background and integrations - [ ] Queued jobs on River (Postgres): CSV import and wishlist digest, plus the `PruneNotifications` and `ReindexAlbums` commands - [ ] Realtime notifications published to the existing Centrifugo server with the same channel names and token issuing, so the Nuxt client keeps working - [ ] Typesense indexing and search for albums with the same query semantics as the Scout-based PHP version - [ ] File uploads and cover images through gocloud.dev/blob with the same public URL shape - [ ] Discogs client and AI cover recognition (Anthropic and OpenAI) with per-user and per-org credentials - [ ] Feedback submissions and sitemap output as in the PHP stack plugins Admin - [ ] Backend admin users with roles, separate from frontend users, as in WinterCMS - [ ] `fields.yaml` and `columns.yaml` parsed (goccy/go-yaml) into a JSON form and list schema served per controller, with the field types Płytarium actually uses (text, textarea, checkbox, switch, dropdown with model-method options, relation with nameFrom/emptyOption) plus layout hints (span, tabs, context, attributes), and a first-class relation-manager schema replacing the one `partial` field - [ ] A minimal Vue 3 + TypeScript admin SPA that renders those schemas as lists and forms for the five Płytarium controllers (Albums, Artists, Collections, Genres, Styles), with types generated from the API's OpenAPI document Quality - [ ] `go vet` and `go test ./...` green at every commit; each phase ends with a unit-test plan bringing its code to full coverage - [ ] Cutover: the Phase 2 parity harness is green on all 154 routes against the ported Go backend (QA-05); the recorder/replayer itself is validated - [ ] Definition of done: `vue-fonoteka-app` and `fonoteka-mcp` run unchanged against the Go backend, in daily use ### Out of Scope - Server-rendered themes and components — second port target keios.eu (`.planning/seeds/keios-port-themes-payments.md`); Płytarium is headless - Payments (paymentgateway, pgstripe) — same second target - Chat, forum, video — wavepath.org territory, later - WASM sandboxed extension API — only after the compiled plugin API is stable for a full milestone (`.planning/seeds/wasm-extension-api.md`) - Runtime plugin loading (stdlib `plugin`, yaegi, RPC plugins) — rejected in `why-go-not-scala.md`; compiled plugins only - MySQL and multi-database support — Postgres only in v1; River needs it and one migration target keeps the port simple - Porting Illuminate module by module — the target app pulls what is needed; unneeded WinterCMS subsystems stay unported - "Improving" API response shapes during the port — parity is the acceptance test; improvements come after cutover - Porting the JVM/Scala modules as code — their designs carry over, their code does not - Sluggable, Sortable, NestedTree, Revisionable model behaviors — Płytarium uses none of them (slugs are hand-rolled in lifecycle hooks); build only when a later port needs them - A generic response envelope or blanket error middleware — the PHP API deliberately has three envelope families (house REST, Laravel-shaped 422 errors, unwrapped RFC 8414/6749 OAuth); unifying them breaks parity ## Context **History.** SummerCMS started in February 2026 as a Scala 3 rewrite (Tapir, Ox, Magnum, Jig). Three infrastructure modules shipped as sbt projects in the meta repo (`../modules/summer-compass`, `summer-phrasebook`, `summer-bonfire`) before the effort stalled: it was building Illuminate-shaped infrastructure bottom-up with no real app pulling requirements, and the deciding test (an OAuth2/OIDC server) found no reusable Scala library. Go passed the same test with zitadel/oidc. The decision is recorded in `.planning/notes/why-go-not-scala.md` and is not to be relitigated. **Prior research.** `.planning/research/go-ecosystem.md` is a verified (2026-09-16, GitHub API) ecosystem report with picks per concern and a plugin-architecture comparison. Project research on 2026-09-16 added `STACK.md`, `FEATURES.md`, `ARCHITECTURE.md`, `PITFALLS.md` and `SUMMARY.md`, all grounded in direct reads of the Płytarium PHP source. Phase research should extend them, not redo them. **Reference implementations.** - WinterCMS starter (the Golem15 stack): `../examples/golem15-wintercms-starter`, with 19 Golem15 plugins as nested submodules and its own CLAUDE.md - Płytarium, the v1 target: `/media/nvme/dev/golem15/fonoteka`. Domain plugin at `plugins/golem15/fonoteka` (25 models, 5 admin controllers, 154 routes in `routes.php`, 27 migrations, 3 commands, 2 jobs, 143 test files, ~57k PHP lines). Nuxt app at `vue-fonoteka-app`, MCP server at `fonoteka-mcp`. Its own GSD history lives in `fonoteka/.planning/`, including `STARTER-LINEAGE.md` - Stack plugins Płytarium uses: user, backend, apparatus, golem (AI), translate, websockets (Centrifugo), userfriends, journal, feedback, sitemap - OAuth2 behavioral reference: `/media/nvme/dev/golem15/wavepath.org/plugins/golem15/oauthserver` (11k lines, tests, `INTEGRATING.md`) - Płytarium's environment today: Centrifugo for realtime (WS URL, API key, proxy secret), Typesense for search, a queue connection, SQLite as the config default with the real connection set in `.env` **Design carry-overs from Scala.** Module names from `../IDEA_LIB_NAMES.md` (backpack, compass, festival, surf, lagoon, bouncer, lifeguard, cooler, conga, postcard, bonfire, sandcastle, phrasebook, sunset, party) become Go package names. Compass's config layering and plugin namespaces, phrasebook's CLDR plurals and namespaced keys, and bonfire's command registry and rich output are language-neutral designs to port. **Go 1.27 notes.** Generic methods, json/v2-backed `encoding/json` (stricter: rejects duplicate keys and invalid UTF-8, relevant to YAML/JSON form definitions), stable iterators and ServeMux patterns. `plugin` package limitations unchanged. **Who works on this.** Golem15 developers and AI agents using the GSD workflow. The repo's `CLAUDE.md` carries lean-mode rules that every phase must follow. ## Constraints - **Tech stack**: Go 1.27, standard library first (net/http ServeMux, html/template, encoding/json); a dependency is added only when the research doc or a phase decision names it — keeps the binary boring and the dependency tree auditable - **Data**: GORM on Postgres only — chosen for Eloquent-like DX and a line-by-line model port; River needs Postgres - **Compatibility**: `vue-fonoteka-app` and `fonoteka-mcp` must run unchanged; the Nuxt app's requests define the contract and response shapes are not improved during the port - **Realtime**: keep the Centrifugo server and port only the publisher and token issuing — the Nuxt client connects to Centrifugo directly - **Plugins**: compiled at build time; no runtime plugin loading without a decision note - **Two repositories**: `summercms.go` is the framework only (the `summer` packages, the CLI, the admin SPA shell, the parity harness tooling) and knows nothing about Płytarium; `fonoteka.go`, a sibling directory in the meta repo, is the application: a go.work workspace holding the ported plugins (user, translate, feedback, sitemap, fonoteka) and the app binary; websockets is not a separate app plugin (D-16, Phase 11): the framework realtime package `lighthouse` plus fonoteka's `config/realtime.yaml`, channel authorizers, `lighthouse.Mount` call and `ws-api` bucket replace it, requiring the framework by module path with a local replace during development. Roadmap phases name which repo each plan writes to; planning docs stay in `summercms.go/.planning` - **Workflow**: lean planning (few, large plans per phase), a plan-count checkpoint before PLAN.md files are written, unit tests as the last plan of every phase, `go vet` and `go test ./...` green at every commit - **Commits**: no co-author tags; one logical change per commit; planning docs and code in separate commits - **Core plugin contracts**: the PHP user, blog, pages and payment plugins are shared across many projects; the Go ports must preserve their contracts and the PHP originals are not changed as part of this project ## Key Decisions | Decision | Rationale | Outcome | |----------|-----------|---------| | Go instead of Scala 3 | Ecosystem depth: the OAuth2/OIDC test had a maintained Go answer (zitadel/oidc) and no Scala one; a CMS is glue over solved problems | ✓ Good (2026-09-16, `notes/why-go-not-scala.md`) | | v1 target is the Płytarium port | Headless, actively used, forces every ecosystem claim at once, three times the size of the `inventory` alternative but finishable | — Pending | | Compiled plugins, Caddy/xcaddy model | Full type safety, plugins extend each other through Go interfaces, `go test` works; rebuild cost is seconds | ✓ Good (Phase 1 hello app, 2026-09-16) | | Headless first; admin is a schema-driven SPA | Płytarium needs no theme engine; PocketBase-style schema-driven admin is the best-fit reference | — Pending | | GORM over ent | Closest to Eloquent's mutable-model DX, simplest line-by-line port of 25 PHP models; ent's codegen deferred | — Pending (chosen at init, 2026-09-16) | | Postgres only for v1 | One migration target; River requires it; Płytarium cuts over as part of go-live | — Pending | | Minimal admin SPA in v1 | Five YAML-driven controllers are part of what Płytarium needs day to day; proves the fields.yaml to JSON schema pipeline | — Pending | | Keep Centrifugo, port the publisher | Nuxt client stays unchanged, which is the definition of done | — Pending | | Reuse IDEA_LIB_NAMES module names as Go packages | Continuity with the Scala work and its docs | — Pending | | WASM extension API deferred | Plugin surface too wide to marshal until the compiled API settles | — Pending (seed) | | Themes and payments deferred to keios.eu port | Smallest Golem15 project with both a Twig theme and a payment flow | — Pending (seed) | | gormigrate over goose for migrations | Plain `[]*gormigrate.Migration` slices compose per plugin at boot and give `RollbackLast()`/`RollbackTo()` on a `*gorm.DB`; goose's provider/FS model fits worse (research STACK.md, 2026-09-16) | ✓ Good (Phase 3: user + fonoteka sets, isolated `migrate:rollback --plugin`) | | koanf + YAML config, goccy/go-yaml everywhere | No maintained Go HOCON parser; `gopkg.in/yaml.v3` upstream archived April 2025; one YAML library for config and admin schemas | — Pending | | swaggo/swag for OpenAPI, not Huma | Comment annotations on plain net/http handlers keep 154 ported routes byte-compatible; Huma would reshape every handler | — Pending | | GORM + River share one `*sql.DB` (pgx stdlib) with a separate pgx pool for LISTEN/NOTIFY | River's documented GORM integration; transactional enqueue inside GORM transactions | ✓ Good for the shared `*sql.DB` seam (Phase 3); River listener pool still Phase 11 | | Parity harness is a day-one workstream | Fixture recording needs only the running PHP backend; it is the acceptance mechanism for every port phase | ✓ Good (Phase 2, 2026-09-17: 154/154 PHP self-replay, Nuxt/MCP fixtures, testcontainers Go runner) | | WinterCMS websockets plugin dissolved into the framework `lighthouse` package (Phase 11 D-16) | Realtime is transport-neutral framework code (drivers, authorizer registry, broadcasts); the app only binds config, the collection/wishlist authorizers, the Mount call, the ws-api bucket and the Album binding, so a separate app plugin would be an empty shell | ✓ Good (Phase 11 plan 11-03) | | Framework and application in two repos from day one (`summercms.go`, `fonoteka.go`) | Retrofitting the split later is more disruptive; shared stack plugins can be extracted for keios.eu without touching the framework; the framework never imports an app | ✓ Good (Phase 3: fonoteka.go workspace, user + fonoteka plugins, app.Handler test seam) | ## Evolution This document evolves at phase transitions and milestone boundaries. **After each phase transition** (via `/gsd-transition`): 1. Requirements invalidated? → Move to Out of Scope with reason 2. Requirements validated? → Move to Validated with phase reference 3. New requirements emerged? → Add to Active 4. Decisions to log? → Add to Key Decisions 5. "What This Is" still accurate? → Update if drifted **After each milestone** (via `/gsd:complete-milestone`): 1. Full review of all sections 2. Core Value check — still the right priority? 3. Audit Out of Scope — reasons still valid? 4. Update Context with current state --- *Last updated: 2026-09-24 after Phase 8 completion*