Jakub Zych 2da8112dbb fix(09): WR-11 enforce case-insensitive unique backend user emails
Add a backend admin migration that creates a unique index on
lower(backend_users.email). Rows copied from WinterCMS may hold emails
that differ only in case, so the migration refuses to run and names the
clashing logins instead of choosing an account to drop.
2026-10-01 23:12:29 +02:00

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 build command reads the application's summer.yaml manifest, 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/http ServeMux, and wire keeps JSON responses byte-compatible with a PHP (WinterCMS/Laravel) backend.
  • Scaffolding CLI. The summer tool builds and watches applications and scaffolds plugins, models, migrations, console commands, jobs and admin controllers. Every application binary also carries runtime commands such as migrate, serve and route:list.

Requirements

  • Go 1.27.
  • PostgreSQL 15 or newer for any application that uses the data layer (lagoon).
  • 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):

  1. Create a database on PostgreSQL 15 or newer:

    CREATE DATABASE hello;
    
  2. Add the request body limits that serve and route:list require. The example does not ship them yet, and surf needs numeric values, which the string-valued environment overlay cannot supply. Create config/http.yaml in examples/hello:

    body_limits:
      default_bytes: 1048576
      upload_bytes: 10485760
    
  3. 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/hello has no http.body_limits configuration, so serve and route:list fail with surf: config http.body_limits.default_bytes is required until you add it as in step 2. The same gap makes the example's TestTypedItemRoute fail.
  • The committed examples/hello/main.go is older than what summer build generates now, so building the example (or running its tests, which build it) leaves that file modified. Restore it with git checkout -- examples/hello/main.go if 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, and src= works on top-level code blocks of docs/ pages only; after changing that source, run summer docs:sync to refresh the copies.

summer docs:serve builds the same site into a temporary directory and previews it at http://127.0.0.1:8088 (--addr to change it), rebuilding when the docs, a module or a src= source changes; a failed rebuild prints its problems and keeps serving the last good build. It listens only on a loopback address unless you pass --allow-remote. Search needs this server: browsers block the search index over file://.

Design notes

Decisions and background live in .planning/notes/, including:

Description
Content management framework for Go,
https://summercms.io
Readme 9.6 MiB
Languages
Go 73.5%
TypeScript 11.3%
Vue 5.7%
Shell 4.3%
JavaScript 2.6%
Other 2.6%