Files
summercms/.planning/PROJECT.md
2026-09-16 03:35:10 +02:00

15 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

(None yet — ship to validate)

Active

Framework kernel

  • 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
  • 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
  • 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; scaffolding commands for plugin, model, controller, migration, command, job
  • i18n with CLDR plurals and namespaced keys (vendor.plugin::group.key), pl and en, the design carried over from summer-phrasebook
  • YAML rules: validation mapped onto go-playground/validator with translated messages

Data layer

  • GORM models with Eloquent-like flows on Postgres; per-plugin gormigrate migration sets runnable up and down, with a "roll back this plugin's last migration and fix it" workflow
  • All 25 Płytarium models ported (Album, Artist, Collection, Genre, Style, AlbumRating, AlbumReservation, ApiToken, CollectionEditor, CollectionInvitation, CsvImport, CsvImportRow, Notification, OAuthAuthCode, OAuthClient, OAuthRefreshToken, OrgAiCredential, OrgDiscogsCredential, PendingInvitationRegistration, Settings, UserAiCredential, UserCollectionContext, UserDiscogsCredential, WishlistDigestQueue, WishlistSubscription) and its 27 migrations
  • Pagination, soft deletes, timestamps, relations (has-many, belongs-to, many-to-many) and JSON columns behave like the PHP originals for the API responses

HTTP and auth

  • net/http ServeMux routing with middleware for auth, org context, locale and CORS; plugins register their routes
  • User plugin port: accounts, registration, login, password reset, JWT for the SPA, organizations and org-scoped permissions
  • OAuth2/OIDC provider (zitadel/oidc) that the MCP server and the ChatGPT connector use with the same flows as today (auth code + PKCE, refresh tokens, client management, IssueOAuthClient command)
  • 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
  • An API parity test suite replays the Nuxt app's and MCP server's requests against both backends and diffs responses
  • 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, websockets, translate, feedback, sitemap, fonoteka) and the app binary, 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 — Pending
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) — Pending
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 — Pending
Parity harness is a day-one workstream Fixture recording needs only the running PHP backend; it is the acceptance mechanism for every port phase — Pending
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 — Pending (chosen 2026-09-16)

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-16 after requirements definition