Initial commit: SummerCMS Go planning docs and research

Records the move from Scala to Go, the compiled-plugin decision, the
Płytarium port as the v1 target, and the Go ecosystem research that
backs the choice. No code yet.
This commit is contained in:
Jakub Zych
2026-09-16 02:07:04 +02:00
commit 8ce164bb0c
9 changed files with 318 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
---
title: v1 target — Płytarium port
date: 2026-09-16
context: Chosen during the opening exploration session over the smaller `inventory` project. Defines v1 scope and the acceptance rule.
---
# v1 target: Płytarium port
**Definition of done for v1:** `vue-fonoteka-app` (Nuxt 4) runs unchanged against the SummerCMS Go backend, and `fonoteka-mcp` talks to it through the same OAuth2 flow it uses today.
Płytarium is the `fonoteka` project at `/media/nvme/dev/golem15/fonoteka`. It is a headless WinterCMS backend for a household record collection, forked from `wn-inventory-plugin`, on the Golem15 starter v1.1.8 LTS.
## Why this one
- Headless. No theme engine needed in v1.
- Actively developed and actually used, so "done" is testable by daily use.
- It forces every ecosystem claim at once: OAuth2/OIDC provider, background jobs, realtime, search, file uploads, external APIs, org-scoped permissions.
- Roughly three times the size of `inventory`, which was the alternative. Large enough to be real, small enough to finish.
## What must be ported
### Domain plugin `golem15.fonoteka`
| Aspect | Count |
|---|---|
| Models | 25 (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) |
| Admin controllers | 5 (Albums, Artists, Collections, Genres, Styles) with YAML-driven forms and lists |
| API routes | 160 (collections, albums, wishlist, sharing, notifications, realtime channels, CSV export, locale, context) |
| Migrations | 27 |
| Console commands | 3 (IssueOAuthClient, PruneNotifications, ReindexAlbums) |
| Jobs | CSV import, wishlist digest |
| Test files | 143 |
| PHP lines | ~57k |
### Stack plugins in play
| Plugin | Role in Płytarium |
|---|---|
| user | Accounts, JWT, organizations, the base of the OAuth2 provider |
| backend | Admin panel improvements |
| apparatus | DI, scenarios, backend tooling |
| golem | AI integration (cover recognition via Anthropic / OpenAI) |
| translate | pl / en |
| websockets | Realtime via Centrifugo |
| userfriends | Friends and groups for collection sharing (optional) |
| journal | Blog / notes (optional) |
| feedback | Feedback submissions |
| sitemap | Sitemap |
### Hard parts, and the Go answer from research
| Hard part | Go pick |
|---|---|
| OAuth2 provider used by ChatGPT connector and MCP | `zitadel/oidc` |
| JWT for the SPA | `golang-jwt/jwt` |
| Queued jobs (CSV import, digests) | `riverqueue/river` (Postgres-backed) |
| Realtime notifications | `coder/websocket`, or keep Centrifugo and port only the publisher |
| Search (Typesense via Scout today) | Typesense Go client |
| File uploads, cover images | `gocloud.dev/blob` |
| Discogs, Anthropic, OpenAI HTTP clients | stdlib `net/http`, official Anthropic Go SDK |
| Validation of YAML `rules:` | `go-playground/validator` |
## Out of scope for v1
- Server-rendered themes and components. Second target: keios.eu (see `../seeds/keios-port-themes-payments.md`).
- Payments (paymentgateway, pgstripe). Same second target.
- Chat, forum, video. wavepath.org territory, later.
- WASM extension API. See `../seeds/wasm-extension-api.md`.
## Reference material
- `wavepath.org/plugins/golem15/oauthserver` (11k lines, has tests and `INTEGRATING.md`) is the fuller OAuth2 implementation in the stack. Use it as the behavioral reference for the user plugin's OIDC port even though Płytarium's own OAuth models are smaller.
- `fonoteka/.planning/` holds the GSD history of the PHP project, including `STARTER-LINEAGE.md`.

View File

@@ -0,0 +1,45 @@
---
title: Why Go, not Scala
date: 2026-09-16
context: Exploration session that opened the summercms.go repository. Records the language decision so it is not relitigated.
---
# Why Go, not Scala
## Background
SummerCMS started in February 2026 as a Scala 3 rewrite of WinterCMS. The stack was direct-style Scala on JDK 21 virtual threads (Tapir, Ox, Magnum, Jig). Three infrastructure modules shipped as independent sbt projects: `summer-compass` (config), `summer-phrasebook` (i18n), `summer-bonfire` (console). They live in the meta repo under `modules/`.
The effort stalled before any application ran. It was building Illuminate-shaped infrastructure bottom-up with no real app pulling requirements.
## The deciding test
We gave both ecosystems the same task: an OAuth2 server with full OIDC support.
- **PHP / WinterCMS.** The agent took `league/oauth2-server` as a model, read the existing Golem15 user plugin forks, reused the JWT primitives already in the stack, and produced a plugin. The Płytarium project today runs its own OAuth2 provider on this basis.
- **Scala.** The agent searched and found nothing reusable. It proposed a from-scratch implementation. The proposal was rejected as too large to fund.
The Scala language was never the problem. The ecosystem was. A CMS is mostly glue over solved problems, and glue needs the problems to be solved already.
## Why Go passes the same test
The Go ecosystem research (see `../research/go-ecosystem.md`) found maintained, widely used libraries for every concern in the Illuminate module map, including the one that failed in Scala: `zitadel/oidc` is an OpenID-certified provider and client library. Go is closer to PHP's situation than Scala is: many small, boring, well-tested libraries, and several Laravel-shaped frameworks and CMSes to read for patterns (Goravel, PocketBase, GoFrame).
## What Go changes
Go is statically compiled, so "drop a plugin folder and it autoloads" does not exist. The decision:
- **Primary: compiled plugins.** Caddy/xcaddy model. `plugins/` is a workspace of Go modules. A generated import list registers them. `summer make:plugin` scaffolds, `summer build` rebuilds, and a watch-rebuild loop in dev makes it feel like WinterCMS. Full type safety, plugins extend each other through ordinary Go interfaces, `go test` works.
- **Later: WASM extension API.** A narrow, sandboxed surface for untrusted third-party extensions. Only after the core plugin API is stable. See `../seeds/wasm-extension-api.md`.
- **Rejected:** stdlib `plugin` (Linux-only, toolchain lockstep, no unload), yaegi (lags Go releases, partial generics), RPC plugins (interface too wide to marshal for model extension).
## What carries over from the Scala work
- The module naming scheme in `IDEA_LIB_NAMES.md` (backpack, compass, phrasebook, bonfire, ...) can be reused as Go package names.
- The design of compass (config layering and plugin namespaces), phrasebook (CLDR plurals, namespaced keys), and bonfire (command registry, rich output) is language-neutral and worth porting.
- The open ORM question from `STACK.md` (immutable models vs Eloquent mutation) largely disappears in Go: structs are mutable, and both `ent` and `GORM` support Eloquent-like flows.
## Non-goals
- Not chasing JVM performance. Go's baseline is enough and deploys as one binary.
- Not porting Illuminate module by module. The v1 target app pulls what is needed. See `v1-target-plytarium.md`.

View File

@@ -0,0 +1,69 @@
---
title: Go ecosystem viability for SummerCMS
date: 2026-09-16
source: Research pass during the opening exploration session. Stars and last-push dates verified via GitHub API on 2026-09-16 unless tagged [ASSUMED].
---
# Go ecosystem viability report (WinterCMS -> Go)
Verdict: the "Go is closer to PHP's richness" bet holds for every concern except the plugin model, and that has a proven answer (section 2).
## 1. Ecosystem coverage
| Concern | Pick (stars, last push) | Runner-up | Maturity note |
|---|---|---|---|
| HTTP router | net/http ServeMux (stdlib; method and wildcard patterns since 1.22) | chi 22.8k (09-2026) | stdlib mux is enough for a CMS; chi adds middleware groups and stays net/http-compatible. gin 89k, echo 32.7k, fiber 40k are alive but use non-stdlib handler types; fiber (fasthttp) breaks stdlib middleware. Avoid. |
| ORM / query | ent 17.2k (09-2026) or GORM 40k (09-2026) | bun 5k, sqlc 18.3k | ent = schema-as-code + codegen, best fit for YAML-driven form generation and migrations. GORM = closest to Eloquent DX, weaker typing. sqlx 17.7k last push 2024-08, stagnant. |
| Migrations | goose 11.5k (09-2026) | golang-migrate 18.9k, atlas 8.7k | goose supports Go-code migrations (needed for plugin migrations). atlas pairs with ent for declarative diff. |
| OAuth2 / OIDC server | zitadel/oidc 1.9k (09-2026), OP+RP, OpenID-certified | go-oauth2/oauth2 3.6k (09-2026, RFC 6749 only, no OIDC) | This is the Scala gap, closed. ory/fosite 2.6k last push 2025-11; Ory pushes users to Hydra. Reference only. |
| JWT | golang-jwt/jwt 9.2k (09-2026) | | De facto standard. |
| Sessions | alexedwards/scs 2.6k (11-2025) | gorilla/sessions 3.2k (08-2024) | scs is server-side with pluggable stores (pg, redis, sqlite). gorilla is cookie-signed and dormant. |
| Validation | go-playground/validator 20.2k (09-2026) | | Tag-based, i18n error messages, maps well to YAML `rules:`. |
| Config | koanf 4.2k (09-2026) | viper 30.5k (01-2026) | koanf is lighter with fewer deps. Either fine. |
| CLI | cobra 44.6k (07-2026) | urfave/cli 24.2k (09-2026) | cobra for `summer make:plugin`-style scaffolding. |
| Templates | html/template (stdlib) + templ 10.5k (09-2026) for typed components | pongo2 3.1k (05-2026) for Twig syntax | pongo2 gives theme authors Twig-like syntax (WinterCMS parity) but is single-maintainer. Use it only for themes, not admin. |
| i18n | go-i18n 3.5k (09-2026) | spreak 94 stars | go-i18n is CLDR-plural, standard. |
| Queue / jobs | river 5.7k (09-2026, Postgres/SQLite, transactional) | asynq 13.7k (06-2026, Redis) | river avoids a Redis dependency. |
| Cache | otter 2.7k (06-2026) in-memory; go-redis 22.2k (09-2026) | ristretto 7k (09-2026) | otter v2 is W-TinyLFU, faster than ristretto per maintainers' benchmarks [ASSUMED]. |
| Mail | wneessen/go-mail 1.5k (09-2026) | | Active, SMTP auth variants, no deps. |
| Events / pubsub | In-process: hand-rolled typed bus with generics | watermill ~8k [ASSUMED] for cross-process | No dominant in-process event lib. It is ~200 LOC. |
| WebSockets | coder/websocket 5.5k (06-2026) | gorilla/websocket 24.9k (03-2025) | coder is context-aware and wasm-capable. gorilla maintained but quiet. |
| File storage | gocloud.dev/blob 9.9k (09-2026) | afero 6.7k (09-2026) | blob = S3/GCS/Azure/local behind one API (Flysystem equivalent). afero for FS mocking. |
## 2. Plugin architecture (key risk)
| Approach | Real project | DX | Verdict |
|---|---|---|---|
| (a) Compile-time registration + custom build | Caddy + xcaddy 1.5k (08-2026); Grafana core; Hugo | Plugin = Go module with `init()` registering into a registry. `summer build --with github.com/x/plugin` rebuilds the binary. | Recommended. Full type safety, zero runtime overhead, plugins extend each other via normal Go interfaces, `go test` works. Pain: rebuild required (seconds), Go toolchain at deploy or in CI. |
| (b) stdlib `plugin` | almost nobody in prod | Linux/macOS only, host and plugin must share exact toolchain and dep versions, no unload, panic kills host | Reject. Docs still say "known to have a number of issues". |
| (c) RPC subprocess | hashicorp/go-plugin 6.1k (09-2026); Grafana backend plugins; Terraform | Each plugin a separate binary over gRPC, 30-50us per call | Good for isolation, wrong for a CMS where plugins register models, form fields, components, and extend each other. Surface too wide to marshal. |
| (d) WASM | extism 5.8k (09-2026) on wazero (~5k [ASSUMED]) | Sandboxed, polyglot, hot-reloadable | Same interface-width problem as (c), plus no direct DB/ORM access from guest. Suitable only for untrusted marketplace "widgets". |
| (e) Interpreter | traefik/yaegi 8.4k, last push 02-2026, README targets Go 1.21/1.22 | Drop source in folder, runs at startup. Traefik vendors deps because yaegi has no module support. | Lagging Go releases badly, partial generics, no cgo, slower. Traefik plugins are tiny middlewares, not CMS plugins. |
Recommendation: (a) as the primary mechanism, mirroring xcaddy: `plugins/` dir of Go modules, a generated `plugins.go` import list, `summer plugin:add` / `make:plugin` commands that scaffold, rewrite go.mod (replace directive for local dev) and rebuild. Add an `air`/reflex-style watch-rebuild for dev to feel like WinterCMS's drop-in loop. Keep an optional (d) WASM slot later for untrusted extensions. This is what Hugo, Caddy, Grafana core, Mattermost (partly), and Gitea converged on.
## 3. Existing Go frameworks and CMSes
| Project | Status | Steal | Base or reference |
|---|---|---|---|
| Goravel (framework 524 stars, skeleton 4.8k, 09-2026) | Alive, active issues 2026 | Facade layer, artisan-style CLI, Laravel-shaped config/queue/mail abstractions | Reference only. Small core team, own ORM wrapper, coupled to its facades. |
| GoFrame 13.3k (09-2026) | Alive, Chinese-led | `gf gen` codegen (dao/model/ctrl), gi18n, config, ORM | Reference. Monolithic, non-idiomatic. |
| Beego 32.4k (09-2026) | Alive but legacy | Little; MVC only | Reference. |
| Buffalo 8.4k | Archived Feb 2024; API shows unarchived, push 03-2026 (unclear) | `pop` ORM migrations, plush templates, generators | Dead for basing. |
| PocketBase 61k (09-2026) | Very alive; "standalone app or Go framework"; SQLite; hooks + JS VM (goja) plugin | Hook/event system, collection-schema-driven admin UI, Go-framework-mode API | Best reference for schema-driven admin forms. Not a base (SQLite-only, no plugin packaging, single maintainer). |
| Fastschema 571 (07-2026) | Beta | ent-backed schema-to-CRUD, plugin system | Reference. Too small. |
| Ponzu 5.8k | Abandoned (README says so) | Nothing | Dead. |
## 4. Go 1.27 (Aug 2026) notes
- Generic methods landed (methods may declare their own type params, not on interfaces). Cleans up repository and query-builder APIs.
- `encoding/json` now backed by json/v2: faster, stricter (rejects duplicate keys and invalid UTF-8). Relevant to YAML/JSON form definitions.
- Iterators (`range over func`, `iter` pkg) stable since 1.23. Use for paginated model queries.
- net/http ServeMux method and wildcard patterns stable since 1.22. 1.27 adds RFC 9218 HTTP/2 priority.
- No change to `plugin` package limitations.
## Flags
- wazero star count unverified.
- Buffalo archived-status contradiction (web says archived 2024, API says not).
- otter-vs-ristretto performance claim is from maintainer benchmarks, not independently verified.

View File

@@ -0,0 +1,25 @@
---
title: keios.eu port — server-rendered themes and payments
trigger_condition: Płytarium v1 is running in production on SummerCMS Go (vue-fonoteka-app unchanged).
planted_date: 2026-09-16
---
# keios.eu port: themes and payments
Second port target after Płytarium. keios.eu is the smallest Golem15 project that has both a Twig theme and a payment flow.
| Aspect | keios.eu |
|---|---|
| Golem15 plugins | apparatus, dualformfield, faq, golem, journal, knobwidget, paymentgateway, pgstripe, quote, sitemap, translate, user, websockets |
| Theme | `wn-keioseu-theme`, 14 template files |
| Plugin PHP lines | ~74k |
## What it adds to SummerCMS
- Theme engine with Twig-like syntax (`pongo2` per research, used only for themes) and components dropped into templates.
- `paymentgateway` and `pgstripe` port. Stripe Go SDK is official.
- `journal`, `faq`, `quote` as classic content plugins with admin forms.
## Related
- `../notes/v1-target-plytarium.md`

View File

@@ -0,0 +1,23 @@
---
title: WASM extension API for untrusted third-party extensions
trigger_condition: Core plugin API (models, form fields, components, events) has been stable for one full milestone and at least two compiled plugins extend each other through it.
planted_date: 2026-09-16
---
# WASM extension API
Decided during the opening exploration: primary plugins compile in (Caddy/xcaddy model), and a second, narrow extension surface runs sandboxed at runtime for third parties who cannot or should not rebuild the binary.
## Shape
- Runtime: `extism` on `wazero` (pure Go, no cgo).
- Surface: deliberately narrow. Hooks on events, custom form field renderers, HTTP handlers under a namespaced prefix, read-only model queries through a host function. No direct DB access, no model extension.
- Packaging: one `.wasm` per extension plus a manifest. Installable from the admin without rebuild.
## Why not now
Both RPC and WASM plugins fail the same way for a CMS core: the plugin surface (register models, extend other plugins' models, add form widgets, add components) is too wide to marshal across a boundary. Only once the compiled API settles do we know which narrow slice is worth exposing.
## Related
- `../notes/why-go-not-scala.md` (plugin decision)