- new note .planning/notes/core-plugins-own-repos.md: shared core plugins live in sm-<name>-plugin repos mounted as submodules - 01-CONTEXT deferral points to the note; PROJECT constraint and Key Decisions row - ROADMAP Phase 12 repos and the 12-01 entry, and Phase 12 plans 12-01, 12-02, 12-05 name sm-user-plugin and the submodule commit workflow
20 KiB
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
- A single
summerbinary 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) - Plugins are Go modules registered at build time through a generated import list;
summer make:pluginscaffolds one andsummer buildrebuilds the binary; a dev watch loop rebuilds on change - A typed in-process event bus lets plugins subscribe to each other's events
- Console command framework with registration, dispatch and rich output (spinners, progress, tables, prompts), the design carried over from summer-bonfire;
summer make:plugin/plugin:addshipped in Phase 1; model, migration, command, job, and admin-controller scaffolds shipped in Phase 4
Validated in Phase 2: API parity harness bootstrap
summer parity:record,parity:replayandparity:proxyon 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- Replay-and-diff against an arbitrary HTTP backend reports JSON-path and byte-offset mismatches; the normalizer fails the named parity classes (nil vs
[], CarbonZvs+00:00, tri-state booleans, envelopes, money string vs number) - App
go test ./paritystarts testcontainers Postgres; unported PHP routes stay pending and never count as a Go pass;scripts/check-phase2.sh --fresh-phpis the sign-off gate
Validated in Phase 3: First vertical slice — genres end to end
- GORM on Postgres through one shared pgx-stdlib
*sql.DB; per-plugin gormigrate sets run up and down inRequiresorder with per-plugin history tables; AutoMigrate is not the schema source (River's LISTEN/NOTIFY pool remains a Phase 11 seam) - Plugins register named middleware and
net/httpServeMux route groups with typed integer params; JWT-guardedGET /_fonoteka/api/v1/genresreturns seeded Genre rows from Postgres - 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.shis the sign-off gate
Validated in Phase 4: CLI scaffolding, i18n and mail
- Scaffolding commands
make:plugin,make:model,make:migration,make:command,make:job, andmake:admin-controlleremit compiling, vet-clean Winter-shaped stubs wired throughregistry.gen.go - 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 - 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.shis the sign-off gate
Validated in Phase 5: Data layer full fidelity
- 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=fonotekaisolates that plugin lagoon.Fillallow-list copy,lagoon.ValidateLaravel rule strings,lagoon.Paginate{data, meta}envelope, AES-256-GCMlagoon.Encrypted, Jsonable TEXT casts, MoneyString 4-decimal numeric, andsystem_filesattach (blob + Winter Thumb names). HTTP public URL serving of uploads remains Phase 6.
Validated in Phase 7: User plugin and authentication
- 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
- Locale resolves per request from preferred_locale then Accept-Language then app.locale, including while the lock is active
surf.ServeCommandandapp.Handlerpublish the uploads*blob.Bucketso assembled avatar POST is 200
Validated in Phase 8: OAuth2.1 authorization server
- 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-clientoperator command, and the/mebootstrap the unchanged fonoteka-mcp process uses - 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.shis 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
wristbandpackage 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
PruneNotificationsandReindexAlbumscommands - 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.yamlandcolumns.yamlparsed (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 onepartialfield- 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 vetandgo 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-appandfonoteka-mcprun 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 inwhy-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 atplugins/golem15/fonoteka(25 models, 5 admin controllers, 154 routes inroutes.php, 27 migrations, 3 commands, 2 jobs, 143 test files, ~57k PHP lines). Nuxt app atvue-fonoteka-app, MCP server atfonoteka-mcp. Its own GSD history lives infonoteka/.planning/, includingSTARTER-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-appandfonoteka-mcpmust 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.gois the framework only (thesummerpackages, 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 application plugins (fonoteka, translate, feedback, sitemap) and the app binary, and mounting shared core plugins from their ownsm-<name>-pluginrepos as git submodules (the user plugin issm-user-plugin, modulegit.golem15.com/golem15/sm-user-plugin, atplugins/golem15/user; see.planning/notes/core-plugins-own-repos.md); websockets is not a separate app plugin (D-16, Phase 11): the framework realtime packagelighthouseplus fonoteka'sconfig/realtime.yaml, channel authorizers,lighthouse.Mountcall andws-apibucket 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 insummercms.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 vetandgo 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) |
Shared core plugins live in their own sm-<name>-plugin repos, mounted into each application as git submodules (first: sm-user-plugin, module git.golem15.com/golem15/sm-user-plugin) |
A second application (sm-summercmsio-app) exists and the Journal port is next; one copy per core plugin instead of forks; supersedes the Phase 1 keios.eu trigger (.planning/notes/core-plugins-own-repos.md) |
✓ Good (2026-10-02, quick 261002-esz) |
Evolution
This document evolves at phase transitions and milestone boundaries.
After each phase transition (via /gsd-transition):
- Requirements invalidated? → Move to Out of Scope with reason
- Requirements validated? → Move to Validated with phase reference
- New requirements emerged? → Add to Active
- Decisions to log? → Add to Key Decisions
- "What This Is" still accurate? → Update if drifted
After each milestone (via /gsd:complete-milestone):
- Full review of all sections
- Core Value check — still the right priority?
- Audit Out of Scope — reasons still valid?
- Update Context with current state
Last updated: 2026-09-24 after Phase 8 completion