Files
summercms/README.md
Jakub Zych 9b3803c098 feat(sunrise): update an application in place with summercms.io/update
- new.sh gains --update, run inside one application directory (go.mod and
  summer.yaml required); it changes only that application
- the framework checkout is read from the app's go.mod replace (single-line
  or block form); module-version replaces, missing targets and foreign or
  dirty checkouts are refused through the shared check_framework_dir
- a detached checkout moves fast-forward only to the newest release (or
  SUMMERCMS_REF) through the shared move_detached; a branch checkout is
  pulled with git pull --no-rebase --ff-only, refusing a missing upstream
  or a diverged branch with the checkout unchanged
- other sm-*-app directories on the same checkout are named on one
  "not rebuilt" line and never built or written
- summer is reinstalled, a missing SUMMER_ADMIN__JWT__SECRET is appended to
  this app's .env (openssl, else head and base64; mode kept, never printed),
  then summer build runs and the script ends with the migrate and restart
  reminder and the shared path_hint
- sunrise.UpdateScript() serves the same script with its last line changed
  to main "$@" --update, so truncated downloads still run nothing
- check-phase14.3.sh --forbidden now allows only the literal
  SUMMER_ADMIN__JWT__SECRET in new.sh and still fails on any other
  SUMMER_ name or on a read or export of one; TestUpdateScript and
  TestUpdateScriptRoute are required tests
2026-10-08 20:15:50 +02:00

171 lines
13 KiB
Markdown

# 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](modules/pact/README.md). 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](modules/party/README.md) 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](modules/cabana/README.md) compiles those definitions at boot and serves them as a JSON admin API, and [boardwalk](modules/boardwalk/README.md) serves the embedded Vue admin SPA that renders them.
- **Headless API.** Plugins declare HTTP routes and middleware. [surf](modules/surf/README.md) assembles them onto a standard library `net/http` ServeMux, and [wire](modules/wire/README.md) keeps JSON responses byte-compatible with a PHP (WinterCMS/Laravel) backend.
- **Scaffolding CLI.** The `summer` tool creates, 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](modules/lagoon/README.md)).
- Node.js 22.6 or newer, only when working on the admin SPA in `admin/`.
- Docker, to run the PostgreSQL 16 database of an application created by `summer new` (its `compose.yaml`), and for the integration tests that start PostgreSQL or Mailpit containers through testcontainers-go.
## Quick start
Create an application with one command. The script checks for Go 1.27 and Docker, clones the framework at its latest release tag into `./summercms`, installs the `summer` CLI and runs `summer new demo`:
```sh
curl -fsSL https://summercms.io/new | bash -s -- demo
cd sm-demo-app
docker compose up -d --wait # PostgreSQL 16 on 127.0.0.1:54320
summer build
summer migrate
summer serve # then: curl http://127.0.0.1:8080/
```
The [sunrise](modules/sunrise/README.md) module holds the script and documents its `SUMMERCMS_REPO`, `SUMMERCMS_DIR` and `SUMMERCMS_REF` overrides. Later, `curl -fsSL https://summercms.io/update | bash` run inside the application moves the framework checkout forward, reinstalls summer and rebuilds that application.
With a framework checkout already in place, install the CLI from it and run `summer new` in the directory next to the checkout; it finds a sibling `summercms` or `summercms.go` checkout, or takes `--framework <path>`:
```sh
go -C summercms install ./cmd/summer
summer new demo
```
### The example application
`examples/hello` is a small application with three compiled plugins. Install the `summer` CLI from the repository root, then build the example:
```sh
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](modules/compass/README.md)):
1. Create a database on PostgreSQL 15 or newer:
```sql
CREATE DATABASE hello;
```
2. Point the binary at the database, give it an application key and an uploads bucket, then migrate and serve:
```sh
./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`, `summer serve`, `summer queue:work`, `summer queue:clear`, `summer schedule:run`, `summer route:list`, `summer key:generate`, `summer admin:create` and `summer admin:reset-password`, run from the application directory, delegate to the built binary in `bin/` and pass their flags to it unchanged.
## 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](modules/backpack/README.md) | Per-instance application container that holds the configuration, a typed service registry, the event bus and the set of activated plugins. |
| [beachcomber](modules/beachcomber/README.md) | Search index sync for GORM models: after-commit upserts and deletes through a pluggable engine, gated by an application kill-switch. |
| [boardwalk](modules/boardwalk/README.md) | HTTP handler that serves the embedded admin SPA build under a configurable path prefix. |
| [bonfire](modules/bonfire/README.md) | Declarative console commands for the `summer` tool and application binaries, adapted to Cobra with typed input, prompts and styled output. |
| [bouncer](modules/bouncer/README.md) | Authentication for SummerCMS: HS256 JWT minting, verification and refresh, request guards, a revoked-token blacklist and bcrypt password helpers. |
| [cabana](modules/cabana/README.md) | 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](modules/compass/README.md) | Layered YAML configuration with per-environment directories, `SUMMER_` environment overrides, embedded plugin defaults and dot-path access. |
| [conga](modules/conga/README.md) | 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](modules/festival/README.md) | Typed, synchronous event bus with listener priorities, payload collection and stop-when-handled dispatch. |
| [fetchguard](modules/fetchguard/README.md) | Guarded outbound HTTP client and fetcher that blocks private and reserved addresses, enforces host, size and timeout limits, and offers an explicit trusted mode for operator-configured endpoints. |
| [flare](modules/flare/README.md) | Web Push delivery with VAPID (RFC 8292) and aes128gcm payload encryption (RFC 8291) behind a small Pusher interface. |
| [lagoon](modules/lagoon/README.md) | Postgres data layer: the shared GORM connection, per-plugin migrations, model helpers and file attachments. |
| [lighthouse](modules/lighthouse/README.md) | Transport-neutral realtime: a publisher interface with pluggable drivers, subscribe-time channel authorization, and model broadcasts enqueued in the write transaction. |
| [pact](modules/pact/README.md) | Capability interfaces that compiled plugins implement to contribute routes, config, migrations, middleware, commands, admin screens, translations, mail templates and jobs. |
| [party](modules/party/README.md) | Compiled plugin registry that orders plugins by their dependencies and runs their Register and Boot lifecycle. |
| [phrasebook](modules/phrasebook/README.md) | Namespaced translation catalogs loaded from plugin YAML, with locale fallback, placeholder interpolation and CLDR pluralization. |
| [postcard](modules/postcard/README.md) | Transactional mail from plugin-owned Markdown templates and layouts, delivered through a memory, log or SMTP driver. |
| [sunrise](modules/sunrise/README.md) | Bootstrap script that installs the summer CLI and creates a new SummerCMS application, or updates an existing one, in one command, embedded so it is versioned with the framework. |
| [sunscreen](modules/sunscreen/README.md) | Credential-redacting slog handler that keeps API keys, tokens and passwords out of application logs. |
| [surf](modules/surf/README.md) | 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](modules/tide/README.md) | HTTP parity toolkit that records request and response fixtures from a reference backend, replays them against a new one and reports normalized differences. |
| [towel](modules/towel/README.md) | Request-scoped actor, organization, collection and locale values carried through `context.Context`. |
| [wire](modules/wire/README.md) | JSON response helpers and value types that keep API bodies byte-compatible with a PHP (WinterCMS/Laravel) backend. |
| [wristband](modules/wristband/README.md) | 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. `summer new` writes this `go.mod` for you, with a relative `replace` to the framework checkout and a committed `go.work` (see [Go modules and workspaces](docs/architecture/go-modules-and-workspaces.md)). Written by hand, 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`](examples/hello/go.mod) and [`examples/hello/summer.yaml`](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:
```sh
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:
```sh
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/`](.planning/notes/), including:
- [Why Go, not Scala](.planning/notes/why-go-not-scala.md)
- [Apparatus dissolved into the framework](.planning/notes/apparatus-dissolved-into-framework.md)
- [Plugin file layout: WinterCMS directories as Go subpackages](.planning/notes/plugin-layout-winter-directories.md)
- [A direct OAuth 2.1-style port: wristband](.planning/notes/oauth2-wristband-direct-port.md)