34 KiB
Stack Research
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
# 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 — exposesRollbackLast()andRollbackTo(id)as first-class methods, plusMigrateTo(id). This is a literal implementation of "drop the last migration and fix it": runRollbackLast(), edit the migration'sMigrate/Rollbackfuncs, runMigrate()again. No separate migration-file format, no separate driver, no SQL string embedding required (thoughtx.Exec(...)is available inside a migration when raw SQL is easier thanMigrator()calls). - goose supports Go-code migrations (not just
.sqlfiles) viagoose.NewGoMigration, which is the reason it was flagged as a candidate. But it registers migrations into its own provider backed by anembed.FSper 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.Migrationslices 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.DBentirely (e.g. a plugin that talks to Postgres via barepgxfor 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:
- Open one
*sql.DBviasql.Open("pgx", dsn)(using pgx'sdatabase/sqlstdlib driver registration,jackc/pgx/v5/stdlib). - Pass that
*sql.DBto GORM:gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{}). - Pass the same
*sql.DBto River via itsriverdatabasesqldriver, usingriverdatabasesql.NewWithPgxListener(sqlDB, listenerPool)— River needsLISTEN/NOTIFYfor real-time job wake-ups, which plaindatabase/sqlcannot expose, so a small separatepgxpool.Poolis 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. - 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. queueReindexAlbumsin 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'sOAuthAuthCode,OAuthClient,OAuthRefreshTokenmodels map onto directly.OPStorage—GetClientByClientID,AuthorizeClientIDSecret,SetUserinfoFromToken,SetIntrospectionFromToken,GetPrivateClaimsFromScopes,GetKeyByIDAndClientID,ValidateJWTProfileScopes.SetUserinfoFromScopesexists but is explicitly documented as deprecated in favor of the optionalCanSetUserinfoFromRequestinterface — 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 (checkwavepath.org/plugins/golem15/oauthserverfor 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:
- swaggo/swag (v1.16.6 stable, 12.9k stars, updated Jul 29, 2026) scans comment annotations above existing
net/httphandlers and generates an OpenAPI/Swagger document. Handlers stay ordinaryfunc(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. - Huma (
danielgtaylor/huma) is framework-agnostic and does support plainhttp.ServeMuxvia itshumagoadapter — but only by making handlers Huma-shaped:func(ctx context.Context, input *Input) (*Output, error)registered throughhuma.Register(api, operation, handler), not idiomatichttp.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. - Hand-written OpenAPI spec (spec-first, e.g. feeding
oapi-codegento 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/postgresfor real-Postgres integration tests: GORM migrations up/down, River job processing (needs realLISTEN/NOTIFY), JSON column round-tripping. Gate behind a build tag/-shortso the fast unit-test loop stays fast. - testify (v1.12.1, Aug 17, 2026) for assertions only (
assert/require) — keep tests as plain stdlibfunc 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 buildof thesummerbinary plus the plugin-import-list regeneration step, not just a barego build ./.... - golangci-lint v2.13.2 (Aug 27, 2026). v2's
.golangci.ymlschema (version: "2") is a breaking change from v1 configs — do not reuse a v1 config verbatim. Configuredepguardto 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-shapedMigrate/Rollbackfuncs; a plugin that deliberately bypasses GORM for a hot path has no natural*gorm.DBto 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/cliinstead 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.DBpattern. 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.goandREADME.md— read directly forRollbackLast/RollbackTo/MigrateToAPI 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,humagoadapter), 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