250 lines
35 KiB
Markdown
250 lines
35 KiB
Markdown
# Stack Research
|
||
|
||
**Phase 1 correction (2026-09-16):** Plugin config uses bare IDs such as `golem15.fonoteka.*` (not `plugins.<name>.*`), and `summer dev` uses built-in `fsnotify` watching (not an external air install). Phase 1 CONTEXT.md D-06 and D-16 supersede the earlier sketches below.
|
||
|
||
**Domain:** Go 1.27 backend framework (WinterCMS/Laravel-shaped CMF), headless-first, compiled plugins, porting a PHP backend (Płytarium) to Go
|
||
**Researched:** 2026-09-16
|
||
**Confidence:** HIGH for versions and maintenance status (checked via pkg.go.dev, GitHub, official docs this session); MEDIUM for architectural recommendations that require judgment calls (migration tool choice, OpenAPI approach, config format)
|
||
|
||
This extends `.planning/research/go-ecosystem.md` (2026-09-16 GitHub-API-verified ecosystem picks). It does not re-litigate that report's ecosystem choices; it adds versions and answers the specific integration questions the milestone raised.
|
||
|
||
## Recommended Stack
|
||
|
||
### Core Technologies
|
||
|
||
| Technology | Version | Purpose | Why Recommended |
|
||
|------------|---------|---------|-----------------|
|
||
| Go | 1.27 (Aug 2026) | Language/runtime | Already decided. Generic methods clean up repository/query-builder APIs; `encoding/json` is now json/v2-backed (stricter: rejects duplicate keys, invalid UTF-8) — matters for hand-authored `fields.yaml`/`columns.yaml` round-tripped through JSON for the admin SPA. |
|
||
| GORM | v1.31.2 (Jun 22, 2026) | ORM, Postgres only | Already decided. Current stable line; v1.31.x added generics-based `Count` etc. Confirmed via pkg.go.dev version list. HIGH confidence. |
|
||
| gorm.io/driver/postgres | v1.6.3 (Sep 14, 2026) | Postgres driver for GORM | Latest patch release, two days before this research. Its `go.mod` pins `github.com/jackc/pgx/v5 v5.10.0` — GORM's "Postgres driver" is pgx under the hood, not lib/pq. This is the crux of the River pool-sharing question below. HIGH confidence (read go.mod directly). |
|
||
| River | v0.47.0 (confirmed via multiple independent 2026 dependabot PRs bumping to this version; GitHub releases page fetch returned stale cached 2024 dates — do not trust that page directly, cross-checked against `riverdriver`/`riverpgxv5`/`riverdatabasesql` sub-package publish dates of Apr–Jul 2026) | Postgres-backed job queue | Already decided. MEDIUM-HIGH confidence on the exact patch version; HIGH confidence it's the current v0.4x line and actively maintained. |
|
||
| zitadel/oidc | v3.51.0 (Sep 14, 2026) | OAuth2/OIDC provider | Already decided, and confirmed correct: **v4 exists only as `v4.0.0-next.4` (pre-release, Jul 30, 2026)**. v3 is the maintained stable line (v3.51.0 shipped two days before this research, newer than the v4 pre-release). Stay on v3; do not chase v4 until it ships stable. Flagged explicitly per the quality gate — this does not contradict the existing decision, it confirms it. |
|
||
| golang-jwt/jwt | v5.3.1 (Jan 28, 2026), import path `github.com/golang-jwt/jwt/v5` | JWT for the SPA and for signing Centrifugo connection/subscription tokens | Already decided. One library covers both jobs — see Centrifugo section. |
|
||
| go-playground/validator | v10.30.4 (Sep 3, 2026), import path `.../validator/v10` | Struct-tag validation from YAML `rules:` | Already decided. Current. |
|
||
| cobra | v1.10.2 (Dec 3, 2025) | CLI framework | Already decided. Current stable. |
|
||
| gocloud.dev | v0.46.0 (Jun 2, 2026) | Blob storage abstraction | Already decided. Current. |
|
||
| Vue 3 + TypeScript | (frontend, out of Go versioning scope) | Minimal admin SPA | Already decided. Types generated from OpenAPI — see OpenAPI section. |
|
||
|
||
### Supporting Libraries
|
||
|
||
| Library | Version | Purpose | When to Use |
|
||
|---------|---------|---------|-------------|
|
||
| go-gormigrate/gormigrate/v2 | latest tag as of May 26, 2026 (1.2k stars, actively maintained, PostgreSQL 18 in its own CI matrix) | Migrations, up/down, per-plugin | **Recommended primary migration tool.** See "Migration tooling" below for why this beats goose and atlas for this project's shape. |
|
||
| pressly/goose | v3.27.3 (Jul 27, 2026, 11.5k stars) | Alternative migration tool | Use instead of gormigrate only if a future plugin needs pure-SQL migrations independent of `*gorm.DB`, or a non-GORM connection. Not recommended as primary — see rationale below. |
|
||
| jackc/pgx/v5 | v5.10.0 (pinned by gorm.io/driver/postgres's own go.mod) | Postgres driver, shared by GORM and River | Not a separate app-level choice — it's already there transitively through GORM's driver. River's own driver (`riverpgxv5`) also needs pgx v5. Pin the same `pgx/v5` version across `go.sum` (Go's module resolution does this automatically via MVS; just don't force a divergent replace directive). |
|
||
| koanf/v2 (`github.com/knadh/koanf/v2`) | v2.3.4 (Mar 21, 2026) | Layered config with plugin namespaces | Already decided per go-ecosystem.md. See "Config format" below for the HOCON question. |
|
||
| koanf providers: `providers/file`, `providers/env`, `providers/confmap` | ships alongside koanf/v2, versioned independently as sub-modules | Base file + env-var overlay + programmatic plugin defaults | `file` provider loads `base.yaml`/`<env>.yaml`; `env` provider overlays `SUMMER_`-prefixed env vars with a transform func mapping `SUMMER_DB__HOST` → `db.host`; `confmap` lets each plugin register its own namespaced defaults (`plugins.<name>.*`) before the file/env layers are merged on top. |
|
||
| koanf/parsers/yaml | tracks koanf/v2 | YAML parsing for koanf | Wraps `goccy/go-yaml` as of koanf's current release — **not** `gopkg.in/yaml.v3`, which is why the direct fields/columns parsing question below matters independently. |
|
||
| goccy/go-yaml | v1.19.2 (Jan 8, 2026) | Parse `fields.yaml` / `columns.yaml` | **Use this, not `gopkg.in/yaml.v3`.** See "What NOT to Use" — yaml.v3's upstream repo (`go-yaml/yaml`) was archived by its maintainer on Apr 1, 2025 and is explicitly marked unmaintained. goccy/go-yaml passes 355/402 cases of the YAML test suite vs 295/402 for yaml.v3, has an AST/tokenizer API useful for round-tripping comments if `summer make:*` scaffolds YAML files, and is what koanf itself has moved to. HIGH confidence — this is a load-bearing finding, not a style preference. |
|
||
| swaggo/swag | v1.16.6 (Jul 29, 2026, stable; v2.0.0-rc6 exists but is not production-ready) | Generate OpenAPI from annotated net/http handlers | **Recommended primary.** See "OpenAPI generation" below. |
|
||
| openapi-typescript | current npm release (JS ecosystem, not Go-versioned) | Generate TS types for the Vue admin SPA from the OpenAPI doc swag produces | Already decided (types generated from OpenAPI). Feeds directly off swag's output JSON/YAML. |
|
||
| typesense-go | v3.2.0 (Mar 27, 2025), confirms Typesense server API v28 support | Typesense client | Already decided. This is the most recent tagged release found; no newer tag surfaced in this research pass — treat the exact patch as MEDIUM confidence (worth a re-check at implementation time since it is over a year old relative to today) but the library itself is the only real Go client and is the correct pick. |
|
||
| centrifugal/gocent/v3 | current release, import path `github.com/centrifugal/gocent/v3` | Centrifugo HTTP API client (publish/broadcast/presence) | Official client from the Centrifugo org. Small (87 stars is normal for a niche official SDK, not a red flag — same maintainers as Centrifugo itself). Given it wraps ~4 REST calls, hand-rolling a thin `net/http` client is a legitimate stdlib-first alternative if the team wants zero non-decided dependencies; gocent is the pragmatic default. |
|
||
| — (no separate library for Centrifugo tokens) | — | Sign Centrifugo connection/subscription JWTs | Centrifugo does not need its own token library — it verifies plain JWTs signed with an HMAC secret (or RSA/ECDSA). **Use the already-decided `golang-jwt/jwt/v5` to sign these tokens too**, with claims shaped per Centrifugo's connection/subscription token spec. One JWT library, two token types (SPA auth, Centrifugo realtime auth). |
|
||
| go-i18n/v2 (`github.com/nicksnyder/go-i18n/v2`) | v2.6.1 (Jan 1, 2026) | CLDR-plural i18n | Already decided. CLDR v48 as of this release (up from CLDR 44 in older v2.3.0 — make sure any tutorial/blog post referencing go-i18n is checked against the current release, plural-rule edge cases have shifted). Also replaced an unmaintained YAML dependency internally in a recent release — another confirmation that the Go YAML-library churn described above is a live, current issue, not stale training-data noise. |
|
||
| testcontainers-go + testcontainers-go/modules/postgres | v0.44.0 (Aug 7, 2026) | Spin up real Postgres in tests | For GORM model tests, migration up/down tests, and River job tests that need real Postgres behavior (JSON columns, `LISTEN`/`NOTIFY`, constraint behavior) rather than SQLite-in-CI approximations. |
|
||
| stretchr/testify | v1.12.1 (Aug 17, 2026) | Assertions in Go tests | Standard choice; use `assert`/`require` for readability in the API-parity diff tests, not as a BDD framework — keep tests as plain `func Test...(*testing.T)`. |
|
||
| air-verse/air | v1.67.4 (Aug 1, 2026), actively maintained | Dev watch-rebuild loop | This is the "watch loop that rebuilds the compiled-plugin binary on change" the plugin architecture already calls for. Config via `.air.toml`; point `cmd`/`bin` at `go build -o ./tmp/summer ./cmd/summer && ./tmp/summer serve`. |
|
||
| golangci-lint | v2.13.2 (Aug 27, 2026) | Linting | v2's config schema (`version: "2"` in `.golangci.yml`) is a breaking change from v1 — do not copy a v1 config from an older Go project without migrating it. |
|
||
|
||
### Development Tools
|
||
|
||
| Tool | Purpose | Notes |
|
||
|------|---------|-------|
|
||
| air | live rebuild in dev | Config as above; combine with the plugin system's own `plugins.go` regeneration step so editing a plugin's route/model file triggers both codegen and rebuild. |
|
||
| golangci-lint v2 | static analysis in CI and pre-commit | Enable `govet`, `staticcheck`, `errcheck`, `revive` at minimum; the stdlib-first constraint makes `depguard` worth configuring to fail CI if an undecided dependency is imported. |
|
||
| testcontainers-go | integration tests against real Postgres | Gate these behind a build tag or `-short` skip so unit tests stay fast; the phase-ending "unit tests" plan (per this repo's lean-mode rule) should still run everywhere, with testcontainers tests as a separate, slower suite. |
|
||
| golden-file API parity tests | diff Go backend responses against recorded PHP responses | Not a library — a pattern: record Płytarium's actual JSON responses per route as fixtures (`testdata/parity/<route>.golden.json`), replay the same requests against the Go backend in `httptest.Server`, diff with `testify/assert.JSONEq` or a small custom normalizer for non-deterministic fields (timestamps, IDs). This is the literal implementation of the PROJECT.md requirement "an API parity test suite replays the Nuxt app's and MCP server's requests against both backends and diffs responses." |
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
# Core (already decided, versions confirmed this session)
|
||
go get gorm.io/gorm@v1.31.2
|
||
go get gorm.io/driver/postgres@v1.6.3
|
||
go get github.com/riverqueue/river@v0.47.0
|
||
go get github.com/riverqueue/river/riverdriver/riverdatabasesql
|
||
go get github.com/riverqueue/river/riverdriver/riverpgxv5 # only if using a raw pgxpool.Pool listener alongside the shared *sql.DB
|
||
go get github.com/zitadel/oidc/v3@v3.51.0
|
||
go get github.com/golang-jwt/jwt/v5@v5.3.1
|
||
go get github.com/go-playground/validator/v10@v10.30.4
|
||
go get github.com/spf13/cobra@v1.10.2
|
||
go get gocloud.dev@v0.46.0
|
||
|
||
# Migrations
|
||
go get github.com/go-gormigrate/gormigrate/v2
|
||
|
||
# Config
|
||
go get github.com/knadh/koanf/v2@v2.3.4
|
||
go get github.com/knadh/koanf/providers/file
|
||
go get github.com/knadh/koanf/providers/env
|
||
go get github.com/knadh/koanf/providers/confmap
|
||
go get github.com/knadh/koanf/parsers/yaml
|
||
|
||
# YAML for fields.yaml / columns.yaml (NOT gopkg.in/yaml.v3)
|
||
go get github.com/goccy/go-yaml@v1.19.2
|
||
|
||
# i18n
|
||
go get github.com/nicksnyder/go-i18n/v2@v2.6.1
|
||
go get golang.org/x/text # CLDR plural support dependency
|
||
|
||
# OpenAPI
|
||
go install github.com/swaggo/swag/cmd/swag@v1.16.6
|
||
|
||
# Search / realtime clients
|
||
go get github.com/typesense/typesense-go/v3@v3.2.0 # confirm exact tag/module path at implementation time
|
||
go get github.com/centrifugal/gocent/v3
|
||
|
||
# Dev / test dependencies
|
||
go get github.com/testcontainers/testcontainers-go@v0.44.0
|
||
go get github.com/testcontainers/testcontainers-go/modules/postgres@v0.44.0
|
||
go get github.com/stretchr/testify@v1.12.1
|
||
|
||
# Dev tools (not go.mod dependencies)
|
||
go install github.com/air-verse/air@v1.67.4
|
||
# golangci-lint installed via its install script, pinned to v2.13.2, not go install
|
||
```
|
||
|
||
## Deep Dives on the Milestone's Specific Questions
|
||
|
||
### GORM: plain structs, not gorm gen or the new GORM CLI
|
||
|
||
GORM now has two competing codegen tools: the older `gorm.io/gen` (full DAO codegen, pre-generics) and the new `github.com/go-gorm/cli` (2026, generics-based, smaller generated surface, GORM's own docs call it the forward direction for new projects). **Neither is recommended for this project's v1.** The explicit design goal is "closest to Eloquent's mutable-model DX, simplest line-by-line port of 25 PHP models" (PROJECT.md) — a straight one-model-to-one-struct port reads and reviews better without a codegen layer in between, and it keeps the dependency tree smaller per the stdlib-first constraint. Revisit `go-gorm/cli` later for new, non-ported plugins if the team wants compile-time-checked queries — that is a second-milestone decision, not a v1 one.
|
||
|
||
### Migration tooling: gormigrate, not goose or atlas
|
||
|
||
The three real options and why gormigrate wins for this specific shape of problem:
|
||
|
||
- **gormigrate** (`go-gorm/gormigrate/v2`) defines migrations as `{ID, Migrate(tx *gorm.DB) error, Rollback(tx *gorm.DB) error}` structs, operates directly on the shared `*gorm.DB`, and — confirmed by reading the source (`gormigrate.go`) directly rather than trusting a README excerpt — exposes `RollbackLast()` and `RollbackTo(id)` as first-class methods, plus `MigrateTo(id)`. This is a literal implementation of "drop the last migration and fix it": run `RollbackLast()`, edit the migration's `Migrate`/`Rollback` funcs, run `Migrate()` again. No separate migration-file format, no separate driver, no SQL string embedding required (though `tx.Exec(...)` is available inside a migration when raw SQL is easier than `Migrator()` calls).
|
||
- **goose** supports Go-code migrations (not just `.sql` files) via `goose.NewGoMigration`, which is the reason it was flagged as a candidate. But it registers migrations into its own provider backed by an `embed.FS` per source tree; making per-plugin migration sets compose cleanly (each plugin contributing its own ordered slice, aggregated by the kernel at boot from the generated plugin import list) is more natural with gormigrate's plain `[]*gormigrate.Migration` slices than with goose's filesystem-and-provider model. goose is still the right tool if a future plugin needs raw-SQL migrations decoupled from `*gorm.DB` entirely (e.g. a plugin that talks to Postgres via bare `pgx` for performance reasons) — keep it as the named fallback, not the default.
|
||
- **atlas** is a declarative schema-diff tool (desired-state HCL/SQL compared against actual state, migration generated automatically). It is the better fit when GORM's own struct tags are treated as the single source of truth and hand-written data-backfill migrations are rare. This project's port needs custom, hand-authored up/down logic per PHP migration (27 of them, with real data semantics, not just DDL) — atlas's diffing model fights that instead of helping it, and it adds a second DSL and a separate binary. Not recommended for v1.
|
||
|
||
**Concrete per-plugin pattern:** each plugin package exposes `var Migrations = []*gormigrate.Migration{...}` with IDs namespaced by plugin (`golem15_fonoteka_20260916143000_create_albums`) to keep global ordering unambiguous when the kernel concatenates every registered plugin's slice, in plugin-registration order, into one `gormigrate.New(db, gormigrate.DefaultOptions, all)` at boot. `summer migrate`, `summer migrate:rollback` (→ `RollbackLast`), and `summer make:migration <plugin> <name>` (scaffolds a Go file with empty `Migrate`/`Rollback` stubs and a timestamp ID) are cobra subcommands wrapping this.
|
||
|
||
### River + GORM: share one `*sql.DB`, not one `pgxpool.Pool`
|
||
|
||
Both GORM's Postgres driver and River need pgx v5 (confirmed: `gorm.io/driver/postgres`'s go.mod pins `jackc/pgx/v5 v5.10.0`), which raises the pooling question directly. River's own documentation (`riverqueue.com/docs/gorm`) answers it: don't try to hand a raw `pgxpool.Pool` to both libraries — instead:
|
||
|
||
1. Open one `*sql.DB` via `sql.Open("pgx", dsn)` (using pgx's `database/sql` stdlib driver registration, `jackc/pgx/v5/stdlib`).
|
||
2. Pass that `*sql.DB` to GORM: `gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})`.
|
||
3. Pass the same `*sql.DB` to River via its `riverdatabasesql` driver, using `riverdatabasesql.NewWithPgxListener(sqlDB, listenerPool)` — River needs `LISTEN`/`NOTIFY` for real-time job wake-ups, which plain `database/sql` cannot expose, so a small separate `pgxpool.Pool` is used *only* for the listener, while all actual queries (both GORM's and River's job table access) go through the one shared `*sql.DB`.
|
||
4. Sharing a GORM transaction with River's job insertion: `tx := gormDB.Begin(); sqlTx := tx.Statement.ConnPool.(*sql.Tx); riverClient.InsertTx(ctx, sqlTx, args, nil); tx.Commit()`. This is how "insert a River job in the same transaction as the GORM write that triggered it" (e.g. queue `ReindexAlbums` in the same transaction as an album save) is done correctly.
|
||
|
||
This is a documented, first-party integration path (River's own docs have a dedicated GORM page), not an improvised workaround — confidence HIGH.
|
||
|
||
### zitadel/oidc v3 storage interface: what to implement
|
||
|
||
Read directly from `pkg/op/storage.go` on the `main` branch (v3 line). The interfaces to implement, split by concern:
|
||
|
||
- **`AuthStorage`** — the core of the authorization-code + PKCE flow: `CreateAuthRequest`, `AuthRequestByID`, `AuthRequestByCode`, `SaveAuthCode`, `DeleteAuthRequest`, `CreateAccessToken`, `CreateAccessAndRefreshTokens`, `TokenRequestByRefreshToken`, `TerminateSession`, `RevokeToken`, `GetRefreshTokenInfo`, `SigningKey`, `SignatureAlgorithms`, `KeySet`. This is the interface Płytarium's `OAuthAuthCode`, `OAuthClient`, `OAuthRefreshToken` models map onto directly.
|
||
- **`OPStorage`** — `GetClientByClientID`, `AuthorizeClientIDSecret`, `SetUserinfoFromToken`, `SetIntrospectionFromToken`, `GetPrivateClaimsFromScopes`, `GetKeyByIDAndClientID`, `ValidateJWTProfileScopes`. `SetUserinfoFromScopes` exists but is explicitly documented as deprecated in favor of the optional `CanSetUserinfoFromRequest` interface — implement the newer one, leave the old one empty.
|
||
- **`ClientCredentialsStorage`** (optional) — needed only if the client-credentials grant is used (worth checking whether the MCP server or ChatGPT connector needs it; Płytarium's OAuth models list doesn't obviously call for it, flag as an open question for the phase that implements this).
|
||
- **`TokenExchangeStorage`** / **`TokenExchangeTokensVerifierStorage`** (optional) — RFC 8693 token exchange; almost certainly out of scope for a straight port unless the existing PHP OAuth server implements it (check `wavepath.org/plugins/golem15/oauthserver` for this before assuming it's unneeded).
|
||
- **Optional `Can*` interfaces** (`CanTerminateSessionFromRequest`, `CanSetUserinfoFromRequest`, and others in the same file) let the request object itself (not just IDs) reach the storage implementation — worth implementing from the start rather than the older, more limited required methods, since they carry richer context.
|
||
|
||
Confirmed: v4 exists only as a pre-release (`v4.0.0-next.4`, Jul 30, 2026); v3 (`v3.51.0`, Sep 14, 2026) is the actively maintained stable line and the correct choice. No contradiction with the existing decision.
|
||
|
||
### Config: koanf + YAML, not a literal HOCON parser
|
||
|
||
The existing decision says "HOCON-style layered config" — read as a design (layering + overlays + namespaces), not a mandate to use literal HOCON syntax, and that reading is the right one. The Go HOCON ecosystem is not viable: `gurkankaymak/hocon` (91 stars, maintenance unclear) and `go-hocon/hocon` (0 stars, 6 commits) are both far below the bar this project applies to every other dependency. There is no maintained, widely used Go HOCON parser to point at.
|
||
|
||
**Recommendation: plain YAML files, layered with koanf.** `base.yaml` + `<env>.yaml` (env overlay) + `plugins/<name>.yaml` (per-plugin namespace, loaded under a `plugins.<name>` key) + an env-var provider (`SUMMER_*`) for secrets/overrides, all merged via successive `koanf.Load()` calls (koanf recursively merges nested maps, later loads win on scalars). This gets HOCON's practical benefits — layering, per-plugin sections, env overlay — without a fragile single-maintainer parser dependency, and it matches YAML already being the framework's admin-schema format (`fields.yaml`, `columns.yaml`), so there's one YAML parser (goccy/go-yaml, via koanf's yaml parser) in the whole binary instead of two config-format parsers. TOML was the other pragmatic option; YAML wins because WinterCMS plugin authors already read/write YAML for `fields.yaml`/`columns.yaml`, so config files look familiar rather than introducing a second syntax.
|
||
|
||
### go-i18n + CLDR plurals
|
||
|
||
`go-i18n/v2` v2.6.1 (Jan 1, 2026) ships CLDR v48 plural rules, generated into the library at build time (not a runtime CLDR database dependency) — this is why go-i18n has no extra runtime dependency on ICU or similar. Namespaced keys (`vendor.plugin::group.key`, per PROJECT.md's phrasebook design carry-over) are an application-level convention on top of go-i18n's flat message-ID space, not a feature go-i18n provides natively — implement the `vendor.plugin::group.key` parsing as a thin lookup layer that resolves to go-i18n message IDs, with per-plugin message bundles merged at boot the same way config namespaces are.
|
||
|
||
### YAML parsing: goccy/go-yaml, confirmed necessary not just preferred
|
||
|
||
This is the one finding in this pass that changes a "nice to have" into a "must": `gopkg.in/yaml.v3`'s upstream (`go-yaml/yaml`) was archived by its sole maintainer on **April 1, 2025**, with an explicit "THIS PROJECT IS UNMAINTAINED" notice in the README (confirmed by reading the repo directly, not a secondhand blog post). It still compiles and works today because YAML parsing is a solved, static problem, but there will be no fixes for edge cases, no security patches, and no Go-version-compatibility work going forward. `goccy/go-yaml` is the community's de facto replacement: actively released (v1.19.2, Jan 8, 2026), higher YAML-test-suite conformance (355/402 vs 295/402), and it's what koanf's own YAML parser has moved to. Use it directly for `fields.yaml`/`columns.yaml` parsing and let koanf pull it in transitively for config. HIGH confidence, verified by reading the archived repo's own README.
|
||
|
||
### OpenAPI generation: swaggo/swag (code-first, comment-driven), not Huma, not hand-written
|
||
|
||
Three real approaches exist:
|
||
|
||
1. **swaggo/swag** (v1.16.6 stable, 12.9k stars, updated Jul 29, 2026) scans comment annotations above existing `net/http` handlers and generates an OpenAPI/Swagger document. Handlers stay ordinary `func(w http.ResponseWriter, r *http.Request)` — **no signature changes**, which is the deciding factor given the stdlib-first constraint and the plugin model ("plugins register their routes" as ordinary handlers). Its v2 line (`v2.0.0-rc6`, Sep 13, 2026) targets OpenAPI 3.1 but is explicitly not production-ready yet — pin v1.16.6.
|
||
2. **Huma** (`danielgtaylor/huma`) is framework-agnostic and does support plain `http.ServeMux` via its `humago` adapter — but only by making handlers Huma-shaped: `func(ctx context.Context, input *Input) (*Output, error)` registered through `huma.Register(api, operation, handler)`, not idiomatic `http.HandlerFunc`. This buys automatic request validation and schema generation from Go types (no comment-drift risk, which is swag's classic failure mode), but it's a bigger structural commitment across every one of 154 routes being ported, and it moves request/response shape control into Huma's conventions right when the whole point of v1 is byte-compatible parity with an existing contract. Reasonable to revisit for new (non-ported) plugins later; not recommended for the parity port itself.
|
||
3. **Hand-written OpenAPI spec** (spec-first, e.g. feeding `oapi-codegen` to generate server stubs) front-loads a lot of authoring work for 154 routes that already need Go handlers written to match PHP responses exactly — writing the spec by hand first and the code second roughly doubles the authoring surface for this specific port. Not recommended at this scale; more attractive for a from-scratch API with far fewer routes.
|
||
|
||
**Recommendation:** swaggo/swag v1.16.6, annotate handlers as they're ported, generate the OpenAPI doc, feed it to `openapi-typescript` for the Vue admin's generated types. Revisit Huma for any genuinely new (non-parity) API surface in a later milestone.
|
||
|
||
### Testing stack
|
||
|
||
- **testcontainers-go** (v0.44.0, Aug 7, 2026) + its `modules/postgres` for real-Postgres integration tests: GORM migrations up/down, River job processing (needs real `LISTEN`/`NOTIFY`), JSON column round-tripping. Gate behind a build tag/`-short` so the fast unit-test loop stays fast.
|
||
- **testify** (v1.12.1, Aug 17, 2026) for assertions only (`assert`/`require`) — keep tests as plain stdlib `func TestX(t *testing.T)`, no BDD DSL, consistent with stdlib-first.
|
||
- **Golden-file API parity tests**: no library needed. Record real Płytarium PHP responses as JSON fixtures per route, replay identical requests against the Go backend via `httptest.NewServer`, diff with a normalizer that ignores non-deterministic fields (timestamps, generated IDs) before comparing. This directly implements the PROJECT.md acceptance test.
|
||
|
||
### Dev loop and linting
|
||
|
||
- **air** (v1.67.4, Aug 1, 2026) for the watch-rebuild loop the compiled-plugin architecture needs to "feel like WinterCMS's drop-in loop" (go-ecosystem.md's own phrase). Point its build command at the full `go build` of the `summer` binary plus the plugin-import-list regeneration step, not just a bare `go build ./...`.
|
||
- **golangci-lint v2.13.2** (Aug 27, 2026). v2's `.golangci.yml` schema (`version: "2"`) is a breaking change from v1 configs — do not reuse a v1 config verbatim. Configure `depguard` to enforce the stdlib-first / decided-dependency-only constraint at CI time, not just by convention.
|
||
|
||
## Alternatives Considered
|
||
|
||
| Recommended | Alternative | When to Use Alternative |
|
||
|--------------|-------------|--------------------------|
|
||
| gormigrate | goose | A future plugin needs raw-SQL migrations independent of `*gorm.DB`/GORM entirely. |
|
||
| gormigrate | atlas | Schema is treated as pure declarative state (GORM struct tags as source of truth) with rare hand-authored data migrations — not this project's shape for v1. |
|
||
| plain GORM structs | go-gorm/cli or gorm.io/gen | A later, non-ported plugin wants compile-time-checked queries and is willing to add a codegen step. |
|
||
| swaggo/swag | huma + humago | A later, non-parity API surface where request/response shape isn't constrained by an existing contract, and the team accepts Huma's handler-shape convention project-wide. |
|
||
| swaggo/swag | hand-written OpenAPI + oapi-codegen | A small, from-scratch API (not a 154-route port) where authoring the spec first is cheaper than annotating existing handlers. |
|
||
| YAML (via koanf) | literal HOCON via a Go parser | Never, for this project — no maintained Go HOCON library exists at the bar this project applies elsewhere. |
|
||
| gocent/v3 | hand-rolled Centrifugo HTTP client | Team wants zero additional non-decided dependencies; Centrifugo's HTTP API is small enough (~4 calls) that this is a legitimate, if slightly more work, stdlib-first option. |
|
||
|
||
## What NOT to Use
|
||
|
||
| Avoid | Why | Use Instead |
|
||
|-------|-----|--------------|
|
||
| `gopkg.in/yaml.v3` (`go-yaml/yaml`) | Archived by its maintainer Apr 1, 2025; README explicitly says "THIS PROJECT IS UNMAINTAINED." No further fixes, security patches, or Go-compatibility work will land. | `goccy/go-yaml` v1.19.2 |
|
||
| `gorm.io/gen` or `go-gorm/cli` for v1 model code | Adds a codegen layer that works against the explicit goal of a simple, reviewable, line-by-line Eloquent-to-GORM port of 25 models. | Plain GORM structs and methods |
|
||
| `atlas` for v1 migrations | Declarative schema-diff model fights hand-authored, data-aware up/down migrations for 27 real PHP migrations with actual data semantics, and adds a second DSL/binary. | gormigrate |
|
||
| Sharing a raw `pgxpool.Pool` directly between GORM's postgres driver and River's `riverpgxv5` driver as two independent pools | Two separate pools against the same database is wasteful and defeats the point of "share one pool"; naively wiring them both to the *same* pool object isn't how either library's constructor is designed to be used. | One shared `*sql.DB` (via `pgx/v5/stdlib`) for both GORM and River's queries, plus one small separate `pgxpool.Pool` used only for River's `LISTEN`/`NOTIFY` wake-ups |
|
||
| zitadel/oidc v4 | Only exists as `v4.0.0-next.4`, a pre-release from Jul 30, 2026 — not production-ready, and older than the current v3.51.0 stable release. | zitadel/oidc v3 (`/v3`), already decided |
|
||
| Huma for the v1 parity port specifically | Requires restructuring every ported handler into Huma's typed Input/Output convention rather than ordinary `net/http` handlers, right when byte-compatible parity with an existing contract is the entire point. | swaggo/swag, annotating existing handlers |
|
||
| swaggo/swag v2 (`v2.0.0-rc6`) | Still a release candidate as of Sep 13, 2026; OpenAPI 3.1 support and dependency cleanup are still in flux. | swaggo/swag v1.16.6 |
|
||
| A literal HOCON parser in Go (`gurkankaymak/hocon`, `go-hocon/hocon`) | Both are tiny, single-or-zero-star projects with unclear or no active maintenance — far below this project's ecosystem-depth bar for every other pick. | koanf + YAML, achieving the same layering design without literal HOCON syntax |
|
||
| golangci-lint v1-style config on a v2 install | v2 changed the `.golangci.yml` schema; a copied v1 config will not behave as expected. | A `version: "2"` config written against v2's current schema |
|
||
|
||
## Stack Patterns by Variant
|
||
|
||
**If a plugin needs raw-SQL migrations outside GORM's model layer (e.g. a performance-critical bulk import path querying via bare `pgx`):**
|
||
- Use goose's Go-code migrations for that plugin specifically
|
||
- Because gormigrate assumes `*gorm.DB`-shaped `Migrate`/`Rollback` funcs; a plugin that deliberately bypasses GORM for a hot path has no natural `*gorm.DB` to hand it
|
||
|
||
**If a later (post-v1) plugin is greenfield rather than a PHP port:**
|
||
- Consider Huma instead of hand-annotated swag comments, and `go-gorm/cli` instead of plain structs
|
||
- Because the byte-compatible-parity constraint that rules both out for v1 doesn't apply to code with no existing contract to match
|
||
|
||
## Version Compatibility
|
||
|
||
| Package A | Compatible With | Notes |
|
||
|-----------|------------------|-------|
|
||
| gorm.io/gorm v1.31.2 | gorm.io/driver/postgres v1.6.3 | Driver's own go.mod targets this gorm version; keep them bumped together. |
|
||
| gorm.io/driver/postgres v1.6.3 | jackc/pgx/v5 v5.10.0 | Pinned directly in the driver's go.mod — this is the pgx version that ends up in `go.sum` for the whole binary via GORM. |
|
||
| River v0.47.0 | jackc/pgx/v5 (River's own `riverpgxv5` driver requires v5; `riverdatabasesql` driver works with any `database/sql`-registered driver, including pgx's `stdlib` registration) | Go's module resolution (MVS) will pick one pgx/v5 version for the whole build; do not force a divergent `replace` for either GORM's or River's sake — let them converge. |
|
||
| zitadel/oidc v3.51.0 | go-jose/go-jose/v4 (imported directly in `pkg/op/storage.go`) | v3's storage interfaces use `jose.SignatureAlgorithm` and `*jose.JSONWebKey` types from go-jose v4 in method signatures — a storage implementation's key-handling code will import this directly. |
|
||
| koanf/v2 v2.3.4 | koanf/parsers/yaml (wraps goccy/go-yaml) | Confirms goccy/go-yaml is already in the dependency tree via koanf; using it directly for `fields.yaml`/`columns.yaml` doesn't add a new library, just a direct dependency on one already present transitively. |
|
||
| go-i18n/v2 v2.6.1 | golang.org/x/text v0.32.0+ | go-i18n's own recent release notes cite bumping to this x/text version alongside the CLDR v48 update. |
|
||
| golangci-lint v2.13.2 | `.golangci.yml` schema `version: "2"` | Not backward compatible with v1-style config files without migration. |
|
||
|
||
## Sources
|
||
|
||
- pkg.go.dev version pages for `gorm.io/gorm`, `gorm.io/driver/postgres`, `github.com/golang-jwt/jwt/v5`, `github.com/go-playground/validator/v10`, `github.com/spf13/cobra`, `gocloud.dev`, `github.com/goccy/go-yaml`, `github.com/stretchr/testify`, `github.com/testcontainers/testcontainers-go` — versions and dates, fetched 2026-09-16. HIGH confidence.
|
||
- `raw.githubusercontent.com/go-gorm/postgres/master/go.mod` — read directly for the pgx/v5 pin. HIGH confidence.
|
||
- `riverqueue.com/docs/gorm` — official River+GORM integration guide, the source for the shared-`*sql.DB` pattern. HIGH confidence.
|
||
- `raw.githubusercontent.com/zitadel/oidc/main/pkg/op/storage.go` — read directly for interface definitions. HIGH confidence.
|
||
- github.com/zitadel/oidc releases page — v3.51.0 vs v4.0.0-next.4 dates. HIGH confidence.
|
||
- `raw.githubusercontent.com/go-gormigrate/gormigrate/master/gormigrate.go` and `README.md` — read directly for `RollbackLast`/`RollbackTo`/`MigrateTo` API and usage pattern. HIGH confidence.
|
||
- github.com/go-yaml/yaml repo page — archived status and "UNMAINTAINED" README notice, read directly. HIGH confidence.
|
||
- WebSearch results on koanf's yaml parser migrating to goccy/go-yaml — MEDIUM confidence (WebSearch-sourced, not independently re-verified against koanf's own go.mod, but corroborated by two independent search results and by go-i18n's own release notes mentioning a similar unmaintained-YAML-dependency swap).
|
||
- github.com/swaggo/swag releases page, huma.rocks docs (`bring-your-own-router`, `humago` adapter), oapi-codegen GitHub — OpenAPI approach comparison. HIGH confidence on version/maintenance facts, MEDIUM-HIGH (judgment call) on the recommendation itself.
|
||
- github.com/gurkankaymak/hocon, github.com/go-hocon/hocon — star counts and activity, read directly. HIGH confidence these are not viable options.
|
||
- github.com/typesense/typesense-go releases — v3.2.0, Mar 27 2025. MEDIUM confidence this is still the latest tag; worth a fresh check at implementation time given the gap since this research date.
|
||
- github.com/centrifugal/gocent repo page — v3 module path, star count. MEDIUM confidence (couldn't confirm an exact latest tag/date from the page content retrieved).
|
||
- github.com/air-verse/air releases — v1.67.4, Aug 1 2026. HIGH confidence.
|
||
- golangci-lint changelog/release references — v2.13.2, Aug 27 2026, v2 config schema change. HIGH confidence.
|
||
|
||
---
|
||
*Stack research for: Go 1.27 CMS/CMF framework (SummerCMS), v1 = Płytarium port*
|
||
*Researched: 2026-09-16*
|