SummerCMS (Go)
SummerCMS is a content management framework for Go, inspired by WinterCMS. An application built on it compiles into a single binary: the framework modules, the application's plugins, the embedded admin SPA and the console commands all ship as one executable.
Key concepts
- Compiled plugins. A plugin is a Go module that implements the capability interfaces in pact. The
summer buildcommand reads the application'ssummer.yamlmanifest, generates the plugin import list and builds the binary. Plugins are registered at build time and never loaded at runtime. party orders them by dependency and runs their register and boot lifecycle. - YAML-driven admin. Plugins describe admin lists, forms, filters and relations in WinterCMS-style YAML (
columns.yaml,fields.yaml). cabana compiles those definitions at boot and serves them as a JSON admin API, and boardwalk serves the embedded Vue admin SPA that renders them. - Headless API. Plugins declare HTTP routes and middleware. surf assembles them onto a standard library
net/httpServeMux, and wire keeps JSON responses byte-compatible with a PHP (WinterCMS/Laravel) backend. - Scaffolding CLI. The
summertool builds and watches applications and scaffolds plugins, models, migrations, console commands, jobs and admin controllers. Every application binary also carries runtime commands such asmigrate,serveandroute:list.
Requirements
- Go 1.27.
- PostgreSQL 16 for any application that uses the data layer (lagoon). lagoon currently requires the database's default locale to be ICU
pl-PL; see Known issues. - Node.js 22.6 or newer, only when working on the admin SPA in
admin/. - Docker, only for the integration tests that start PostgreSQL or Mailpit containers through testcontainers-go.
Quick start
examples/hello is a small application with three compiled plugins. Install the summer CLI from the repository root, then build the example:
go install ./cmd/summer
cd examples/hello
summer build # generates plugins.gen.go and main.go, writes bin/hello
./bin/hello --help
./bin/hello greeter:hello
greeter:hello prints the example's layered configuration and needs no database. key:generate also runs without a database.
The migrate, migrate:status, migrate:rollback, serve and admin commands open PostgreSQL, so they need more configuration. Configuration comes from YAML files in config/, and any key can be overridden with a SUMMER_-prefixed environment variable in which a double underscore separates path segments (see compass):
-
Create a PostgreSQL 16 database with the locale lagoon checks for:
CREATE DATABASE hello TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'; -
Add the request body limits that
serveandroute:listrequire. The example does not ship them yet, and surf needs numeric values, which the string-valued environment overlay cannot supply. Createconfig/http.yamlinexamples/hello:body_limits: default_bytes: 1048576 upload_bytes: 10485760 -
Point the binary at the database, give it an application key and an uploads bucket, then migrate and serve:
./bin/hello key:generate # prints a base64 key; copy the value export SUMMER_DATABASE__DSN='postgres://acme:<secret>@127.0.0.1:5432/hello?sslmode=disable' export SUMMER_APP__KEY='<value printed by key:generate>' export SUMMER_STORAGE__UPLOADS__BUCKET_URL='mem://' ./bin/hello migrate ./bin/hello route:list ./bin/hello serve --addr 127.0.0.1:8080 curl http://127.0.0.1:8080/items/1
summer migrate, summer migrate:status, summer migrate:rollback and summer serve, run from the application directory, delegate to the built binary in bin/.
Known issues
examples/hellohas nohttp.body_limitsconfiguration, soserveandroute:listfail withsurf: config http.body_limits.default_bytes is requireduntil you add it as in step 2. The same gap makes the example'sTestTypedItemRoutefail.- lagoon refuses any database whose default locale is not ICU
pl-PL. - The committed
examples/hello/main.gois older than whatsummer buildgenerates now, so building the example (or running its tests, which build it) leaves that file modified. Restore it withgit checkout -- examples/hello/main.goif you do not intend to commit it.
Repository layout
| Path | Contents |
|---|---|
admin/ |
The Vue 3 and TypeScript admin SPA; its build output is embedded by boardwalk. |
cmd/ |
The summer CLI (cmd/summer). |
docs/ |
Documentation source; summer docs:build renders it into a static site. |
examples/ |
Example applications; examples/hello is the reference application and workspace. |
internal/ |
CLI internals: the build and scaffolding generator, the watch loop and an OpenAPI conversion tool. |
modules/ |
The framework modules, one Go package each, listed below. |
scripts/ |
Verification scripts used as phase gates. |
Modules
| Module | Purpose |
|---|---|
| backpack | Per-instance application container that holds the configuration, a typed service registry, the event bus and the set of activated plugins. |
| beachcomber | Search index sync for GORM models: after-commit upserts and deletes through a pluggable engine, gated by an application kill-switch. |
| boardwalk | HTTP handler that serves the embedded admin SPA build under a configurable path prefix. |
| bonfire | Declarative console commands for the summer tool and application binaries, adapted to Cobra with typed input, prompts and styled output. |
| bouncer | Authentication for SummerCMS: HS256 JWT minting, verification and refresh, request guards, a revoked-token blacklist and bcrypt password helpers. |
| cabana | Schema-driven admin backend that compiles WinterCMS-style YAML list, form, filter and relation definitions at boot and serves them as a JSON admin API next to the embedded admin SPA. |
| compass | Layered YAML configuration with per-environment directories, SUMMER_ environment overrides, embedded plugin defaults and dot-path access. |
| conga | Background jobs on River over the shared Postgres pool: transactional dispatch, a summer_jobs progress record, in-process or dedicated workers, and a wall-clock scheduler. |
| festival | Typed, synchronous event bus with listener priorities, payload collection and stop-when-handled dispatch. |
| fetchguard | Guarded outbound HTTPS fetcher that blocks private and reserved addresses and enforces host, size and timeout limits. |
| flare | Web Push delivery with VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291) behind a small Pusher interface. |
| lagoon | Postgres data layer: the shared GORM connection, per-plugin migrations, model helpers and file attachments. |
| lighthouse | Transport-neutral realtime: a publisher interface with pluggable drivers, subscribe-time channel authorization, and model broadcasts enqueued in the write transaction. |
| pact | Capability interfaces that compiled plugins implement to contribute routes, config, migrations, middleware, commands, admin screens, translations, mail templates and jobs. |
| party | Compiled plugin registry that orders plugins by their dependencies and runs their Register and Boot lifecycle. |
| phrasebook | Namespaced translation catalogs loaded from plugin YAML, with locale fallback, placeholder interpolation and CLDR pluralization. |
| postcard | Transactional mail from plugin-owned Markdown templates and layouts, delivered through a memory, log or SMTP driver. |
| surf | HTTP routing for SummerCMS: collects plugin routes and named middleware into a net/http ServeMux with constraints, rate limiting, body limits, CORS and panic recovery, and provides the serve and route:list commands. |
| tide | HTTP parity toolkit that records request and response fixtures from a reference backend, replays them against a new one and reports normalized differences. |
| towel | Request-scoped actor, organization, collection and locale values carried through context.Context. |
| wire | JSON response helpers and value types that keep API bodies byte-compatible with a PHP (WinterCMS/Laravel) backend. |
| wristband | An OAuth authorization server for MCP clients: RFC 8414 metadata, RFC 7591 dynamic client registration, the authorization-code flow with PKCE, consent operations and refresh-token rotation over application-supplied storage. |
Using SummerCMS in an application
The framework is a single Go module, git.golem15.com/golem15/summercms; each framework package is imported from git.golem15.com/golem15/summercms/modules/<name>. An application requires that module and, during development, points it at a local checkout with a replace directive. For an application module acme in a sibling directory:
module git.golem15.com/acme/acme
go 1.27.0
require git.golem15.com/golem15/summercms v0.0.0
replace git.golem15.com/golem15/summercms => ../summercms.go
The application lists its plugins in summer.yaml (module path, binary name, plugin ids and module paths); summer plugin:add registers a local plugin module in that manifest and in the workspace, and summer make:plugin scaffolds a new one. examples/hello/go.mod and examples/hello/summer.yaml are the working reference: the example replaces the framework with ../.. and its plugin modules with local paths.
Development
Run these from the repository root:
go vet ./...
go test ./... # full suite; the Docker-backed integration tests start containers
go test -short ./... # fast loop without Docker; skips the testcontainers-backed tests
go test ./... at the root does not include examples/hello, which is a separate workspace module; run its tests with cd examples/hello && go test ./....
The admin SPA lives in admin/. Install its dependencies with npm --prefix admin ci, then use its scripts:
npm --prefix admin run dev # Vite dev server
npm --prefix admin run build # type-check, then build into modules/boardwalk/dist
npm --prefix admin run typecheck # vue-tsc only
npm --prefix admin run test # Vitest
npm --prefix admin run gen:api # regenerate TypeScript types from admin/openapi/admin.json
summer dev watches an application's sources and rebuilds its binary on change.
summer docs:build renders docs/ and every module README into a static site under site/ (use --out for another directory, --check to validate without writing). Code blocks with a src= reference are copies of real source; after changing that source, run summer docs:sync to refresh the copies.
Design notes
Decisions and background live in .planning/notes/, including: