Files
summercms/.planning/PROJECT.md
Jakub Zych 947aabee93 docs: move golem15.user to sm-user-plugin and supersede the Phase 1 extraction deferral
- 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
2026-10-02 11:02:07 +02:00

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

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

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

  • 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
  • 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.sh is 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=fonoteka isolates that plugin
  • 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

  • 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.ServeCommand and app.Handler publish the uploads *blob.Bucket so 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-client operator command, and the /me bootstrap 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.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 application plugins (fonoteka, translate, feedback, sitemap) and the app binary, and mounting shared core plugins from their own sm-<name>-plugin repos as git submodules (the user plugin is sm-user-plugin, module git.golem15.com/golem15/sm-user-plugin, at plugins/golem15/user; see .planning/notes/core-plugins-own-repos.md); 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)
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):

  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