- 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
185 lines
20 KiB
Markdown
185 lines
20 KiB
Markdown
# 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 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*
|