feat(build): make summer new apps admin-ready

- NewOptions.AdminSecret: summer new writes SUMMER_ADMIN__JWT__SECRET
  to .env (still 0600), generated from 32 crypto/rand bytes when empty
  and validated as at least 32 .env-safe characters
- .env.example gets an empty SUMMER_ADMIN__JWT__SECRET= placeholder
  with a comment; the secret never reaches the printed next steps
- The next steps and the generated CLAUDE.md explain the admin at
  /backend: make:admin-controller, build, migrate and
  summer admin:create --email you@example.com --superuser
- Goldens use a fixed, non-secret admin secret fixture
- Scaffolding, installation and users-and-permissions docs updated
This commit is contained in:
Jakub Zych
2026-10-08 17:20:19 +02:00
parent da21670870
commit f70d80ec7e
13 changed files with 264 additions and 36 deletions

View File

@@ -32,6 +32,8 @@ login:
decay_minutes: 1
```
An application created with `summer new` already has a random `SUMMER_ADMIN__JWT__SECRET` in `.env`.
The cookie carries the Secure attribute. `backend.cookie_secure: false` drops it for plain-HTTP development and is refused in the `production` environment.
## Permissions

View File

@@ -46,10 +46,10 @@ The application gets these files:
| `go.work` | The workspace with the application and its plugin modules, as relative paths. It is committed. |
| `summer.yaml` | The manifest: module path, binary name and plugin list. |
| `config/` | `app.yaml`, `database.yaml`, `http.yaml` and `storage.yaml`, enough for `serve` and `route:list`. |
| `.env`, `.env.example` | The database DSN and a freshly generated application key. `.env` is readable by its owner only and never committed; `.env.example` lists the variables without the key. |
| `.env`, `.env.example` | The database DSN, a freshly generated application key and a random admin token secret (`SUMMER_ADMIN__JWT__SECRET`). `.env` is readable by its owner only and never committed; `.env.example` lists the variables without the key and the secret. |
| `compose.yaml` | PostgreSQL 16 on `127.0.0.1:54320`, with credentials matching `.env`. |
| `.gitignore` | `bin/`, `tmp/`, `storage/`, `.env` and `go.work.sum`. |
| `CLAUDE.md`, `AGENTS.md` | Instructions for AI coding agents: the commands, the layout, the links to the documentation for agents and the naming convention. `AGENTS.md` is a symlink to `CLAUDE.md`. |
| `CLAUDE.md`, `AGENTS.md` | Instructions for AI coding agents: the commands, the layout, the admin, the links to the documentation for agents and the naming convention. `AGENTS.md` is a symlink to `CLAUDE.md`. |
| `plugins/<name>` | The starter plugin `golem15.<name>`, created by the same code as `summer make:plugin` and registered by the same code as `summer plugin:add`. Its `routes.go` answers `GET /` with a JSON greeting. |
| `main.go`, `plugins.gen.go` | Generated from `summer.yaml`, as `summer build` does. |
@@ -62,6 +62,17 @@ summer migrate
summer serve
```
The next steps also explain the admin. It opens at `/backend` once a plugin registers an admin controller; until then `summer serve` prints `admin: off` with these steps:
```sh
summer make:admin-controller golem15.acme Posts
summer build
summer migrate
summer admin:create --email you@example.com --superuser
```
[Admin controllers](../backend/admin-controllers.md) explains the permission and model the generated controller needs.
Golem15 names repositories by what they hold: `sm-<name>-app` for an application such as the one `summer new` writes, `sm-<name>-plugin` for a standalone plugin module and `sm-<name>-theme` for a Nuxt or Vue frontend.
## Creating a plugin

View File

@@ -48,7 +48,7 @@ Set them on the `bash` side of the pipe, for example `curl -fsSL https://summerc
The generated application brings its own database and configuration:
- `compose.yaml` runs PostgreSQL 16, published on `127.0.0.1:54320` only, so it does not clash with a PostgreSQL already on port 5432.
- `.env` holds the matching `SUMMER_DATABASE__DSN` and a freshly generated `SUMMER_APP__KEY`. It is excluded by `.gitignore`; `.env.example` lists the same variables.
- `.env` holds the matching `SUMMER_DATABASE__DSN`, a freshly generated `SUMMER_APP__KEY` and a random `SUMMER_ADMIN__JWT__SECRET` for the admin. It is excluded by `.gitignore`; `.env.example` lists the same variables.
- `config/` holds `app.yaml`, `database.yaml`, `http.yaml` and `storage.yaml`, enough for `summer serve` and `route:list`.
From `sm-demo-app`, start the database, build the binary, run the migrations and serve:
@@ -68,6 +68,14 @@ curl http://127.0.0.1:8080/
It returns `{"message":"Hello from SummerCMS","plugin":"golem15.demo"}`. Stop the database with `docker compose down`, or `docker compose down -v` to delete its data as well.
`summer serve` prints the URL to open and the admin status. The admin lives at `http://127.0.0.1:8080/backend` once a plugin registers an admin controller; until then the status line reads `admin: off` and names the steps. When the admin is there, create the first administrator:
```sh
summer admin:create --email you@example.com --superuser
```
[Admin controllers](../backend/admin-controllers.md) shows how to add a controller, and [Users and permissions](../backend/users-and-permissions.md) how administrators sign in.
## Create an application without the script
When you already have a framework checkout, or want to read the code before running anything, do the script's steps yourself. Clone the framework into `summercms`, install the tool from it and run `summer new` in the directory next to the checkout:

View File

@@ -3,6 +3,7 @@ package build
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
@@ -38,6 +39,10 @@ type NewOptions struct {
// AppKey is written to .env as SUMMER_APP__KEY. It must be the base64
// (standard encoding) of 32 bytes; the CLI passes lagoon.GenerateAppKey.
AppKey string
// AdminSecret is written to .env as SUMMER_ADMIN__JWT__SECRET, the HMAC
// secret of admin tokens (admin.jwt.secret). Empty means NewApp
// generates the base64 of 32 crypto/rand bytes.
AdminSecret string
// NoGit skips git init and the first commit.
NoGit bool
}
@@ -77,15 +82,16 @@ type genFile struct {
// newData feeds the new/* templates.
type newData struct {
Name string
Module string
Framework string
GoVersion string
Toolchain string
Replace string
DSN string
AppKey string
NoPlugin bool
Name string
Module string
Framework string
GoVersion string
Toolchain string
Replace string
DSN string
AppKey string
AdminSecret string
NoPlugin bool
}
// ValidateAppName reports whether name is usable as `summer new <name>`.
@@ -127,6 +133,16 @@ func NewApp(ctx context.Context, opts NewOptions, out io.Writer) (string, error)
if err := validateAppKey(opts.AppKey); err != nil {
return "", err
}
if opts.AdminSecret == "" {
secret, err := generateAdminSecret()
if err != nil {
return "", fmt.Errorf("build: generate admin secret: %w", err)
}
opts.AdminSecret = secret
}
if err := validateAdminSecret(opts.AdminSecret); err != nil {
return "", err
}
parent := opts.Parent
if parent == "" {
parent = "."
@@ -191,6 +207,29 @@ func validateAppKey(key string) error {
return nil
}
// adminSecretRe is the character set an admin secret may use: it is written
// unquoted into .env, whose reader trims whitespace and strips quotes.
var adminSecretRe = regexp.MustCompile(`^[A-Za-z0-9+/=_-]{32,}$`)
// generateAdminSecret returns the base64 (standard encoding) of 32
// crypto/rand bytes.
func generateAdminSecret() (string, error) {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(raw), nil
}
// validateAdminSecret requires at least 32 characters that are safe to
// write unquoted into .env.
func validateAdminSecret(s string) error {
if !adminSecretRe.MatchString(s) {
return fmt.Errorf("build: admin secret must be at least 32 characters of letters, digits, +, /, =, _ or -")
}
return nil
}
// devDSN is the .env database DSN matching compose.yaml.
func devDSN(name string) string {
return "postgres://" + devDBUser + ":" + devDBPassword + "@" + devDBHostPort + "/" + name + "?sslmode=disable"
@@ -318,6 +357,9 @@ func planNewApp(opts NewOptions, target, framework string) ([]genFile, error) {
if err := validateAppKey(opts.AppKey); err != nil {
return nil, err
}
if err := validateAdminSecret(opts.AdminSecret); err != nil {
return nil, err
}
if err := checkFramework(framework); err != nil {
return nil, fmt.Errorf("build: %w", err)
}
@@ -326,15 +368,16 @@ func planNewApp(opts NewOptions, target, framework string) ([]genFile, error) {
return nil, fmt.Errorf("build: framework replace: %w", err)
}
data := newData{
Name: opts.Name,
Module: appModulePath(opts.Name),
Framework: frameworkModule,
GoVersion: goVersionLine,
Toolchain: toolchainLine,
Replace: replace,
DSN: devDSN(opts.Name),
AppKey: opts.AppKey,
NoPlugin: opts.NoPlugin,
Name: opts.Name,
Module: appModulePath(opts.Name),
Framework: frameworkModule,
GoVersion: goVersionLine,
Toolchain: toolchainLine,
Replace: replace,
DSN: devDSN(opts.Name),
AppKey: opts.AppKey,
AdminSecret: opts.AdminSecret,
NoPlugin: opts.NoPlugin,
}
plan := []struct {
rel string

View File

@@ -22,6 +22,16 @@ var goldenAppKey = func() string {
return base64.StdEncoding.EncodeToString(raw)
}()
// goldenAdminSecret is the base64 of the bytes 0x20..0x3f: a fixed,
// non-secret admin token secret that keeps .env byte-stable in the goldens.
var goldenAdminSecret = func() string {
raw := make([]byte, 32)
for i := range raw {
raw[i] = byte(0x20 + i)
}
return base64.StdEncoding.EncodeToString(raw)
}()
// newGoldenApp runs NewApp for "demo" in a temp parent whose sibling
// summercms is a symlink to this repository, so auto-detection yields the
// relative replace ../summercms. It returns the app dir and the output with
@@ -38,11 +48,12 @@ func newGoldenApp(t *testing.T, noPlugin bool) (string, []byte) {
}
var out bytes.Buffer
dir, err := NewApp(t.Context(), NewOptions{
Name: "demo",
Parent: parent,
AppKey: goldenAppKey,
NoGit: true,
NoPlugin: noPlugin,
Name: "demo",
Parent: parent,
AppKey: goldenAppKey,
AdminSecret: goldenAdminSecret,
NoGit: true,
NoPlugin: noPlugin,
}, &out)
if err != nil {
t.Fatalf("NewApp: %v\n%s", err, out.String())

View File

@@ -2,6 +2,7 @@ package build
import (
"bytes"
"encoding/base64"
"go/token"
"io/fs"
"os"
@@ -642,3 +643,88 @@ func TestNewNoGit(t *testing.T) {
t.Fatalf("output lacks the created line:\n%s", out.String())
}
}
// envValue returns the value of key in an env file body, or "" and false.
func envValue(body []byte, key string) (string, bool) {
for _, line := range strings.Split(string(body), "\n") {
if v, ok := strings.CutPrefix(line, key+"="); ok {
return v, true
}
}
return "", false
}
// TestNewAdminSecret writes SUMMER_ADMIN__JWT__SECRET to .env only, generates
// a random one when none is given and rejects values unsafe for .env.
func TestNewAdminSecret(t *testing.T) {
const key = "SUMMER_ADMIN__JWT__SECRET"
t.Run("explicit", func(t *testing.T) {
const secret = "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWY="
parent := parentWithFramework(t)
var out bytes.Buffer
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, AdminSecret: secret, NoGit: true, NoPlugin: true}, &out)
if err != nil {
t.Fatal(err)
}
env := readAppFile(t, dir, ".env")
if !strings.Contains(string(env), "\n"+key+"="+secret+"\n") {
t.Fatalf(".env does not hold %s=%s:\n%s", key, secret, env)
}
st, err := os.Stat(filepath.Join(dir, ".env"))
if err != nil {
t.Fatal(err)
}
if st.Mode().Perm() != 0o600 {
t.Fatalf(".env mode = %o, want 600", st.Mode().Perm())
}
example := readAppFile(t, dir, ".env.example")
if v, ok := envValue(example, key); !ok || v != "" {
t.Fatalf(".env.example %s = (%q, %v), want an empty placeholder", key, v, ok)
}
if strings.Contains(string(example), secret) {
t.Fatal(".env.example contains the admin secret")
}
if strings.Contains(out.String(), secret) {
t.Fatal("the next steps print the admin secret")
}
})
t.Run("generated", func(t *testing.T) {
var seen []string
for range 2 {
parent := parentWithFramework(t)
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil)
if err != nil {
t.Fatal(err)
}
v, ok := envValue(readAppFile(t, dir, ".env"), key)
if !ok {
t.Fatalf(".env has no %s", key)
}
raw, err := base64.StdEncoding.DecodeString(v)
if err != nil || len(raw) != 32 {
t.Fatalf("generated secret %q is not the base64 of 32 bytes (%v)", v, err)
}
seen = append(seen, v)
}
if seen[0] == seen[1] {
t.Fatal("two apps got the same admin secret")
}
})
t.Run("invalid", func(t *testing.T) {
for _, secret := range []string{
"short",
"abcdefghijklmnopqrst uvwxyzABCDEFGHIJKLMN",
"abcdefghijklmnopqrst\nuvwxyzABCDEFGHIJKLMN",
} {
parent := parentWithFramework(t)
_, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, AdminSecret: secret, NoGit: true, NoPlugin: true}, nil)
if err == nil || !strings.Contains(err.Error(), "admin secret") {
t.Fatalf("NewApp(AdminSecret %q) = %v, want an admin secret error", secret, err)
}
assertDirEntries(t, parent, []string{"summercms"})
}
})
}

View File

@@ -34,11 +34,12 @@ uploads:
public_path_prefix: "/storage/uploads"
{{end}}
{{define "new/env"}}# Local secrets for this application: the database DSN and the application
# key. .gitignore excludes this file; never commit it. Real environment
# variables override the values here.
{{define "new/env"}}# Local secrets for this application: the database DSN, the application key
# and the admin token secret. .gitignore excludes this file; never commit it.
# Real environment variables override the values here.
SUMMER_DATABASE__DSN={{.DSN}}
SUMMER_APP__KEY={{.AppKey}}
SUMMER_ADMIN__JWT__SECRET={{.AdminSecret}}
{{end}}
{{define "new/env.example"}}# The variables .env sets. summer new wrote a .env with a fresh key; copy this
@@ -47,6 +48,10 @@ SUMMER_DATABASE__DSN={{.DSN}}
# A 32-byte base64 application key. Generate one with ./bin/{{.Name}} key:generate
# and paste only the key, without the leading check mark.
SUMMER_APP__KEY=
# Signs backend admin tokens; the admin refuses to start without it once a
# plugin registers an admin controller. summer new wrote a random value to .env;
# any long random string works, for example one from ./bin/{{.Name}} key:generate.
SUMMER_ADMIN__JWT__SECRET=
# Uploads bucket; config/storage.yaml already sets this default.
# SUMMER_STORAGE__UPLOADS__BUCKET_URL=file://./storage/app/uploads
# Configuration environment; selects the config overlay (default production).
@@ -110,6 +115,8 @@ summer migrate # run plugin migrations
summer serve # start the HTTP server
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
```
## Layout
@@ -117,13 +124,23 @@ summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.w
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
- `plugins/`: local plugin modules.{{if not .NoPlugin}} `plugins/{{.Name}}` is the starter plugin `golem15.{{.Name}}`; its `routes.go` answers `GET /` with JSON.{{end}}
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
- `.env`: local secrets (database DSN, application key). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `compose.yaml`: the local PostgreSQL 16.
- `main.go`, `plugins.gen.go`: generated by `summer build` from `summer.yaml`. Do not edit them by hand; they are committed.
- `go.work`: the workspace that joins the application and its plugin modules.
Plugins are Go modules compiled into the binary at build time; there is no runtime plugin loading. Add a plugin with `summer make:plugin` and `summer plugin:add`, then run `summer build`.
## Admin
The backend admin (the admin SPA and its JSON API) lives under `/backend`, at `http://127.0.0.1:8080/backend` with the default `summer serve` address. It is mounted only when a plugin registers an admin controller; until then `summer serve` prints `admin: off`. To add it:
1. `summer make:admin-controller golem15.<plugin> <Name>` scaffolds the controller and its YAML. The generated controller requires the permission `golem15.<plugin>.access_<name>` (`<name>` is the controller name in snake case): declare it in the plugin's `Permissions()` (`pact.HasPermissions`) or the admin refuses to start. Its `NewRecord` returns `nil` until you return the plugin's model.
2. `summer build`, then `summer migrate`.
3. `summer admin:create --email you@example.com --superuser` creates the first administrator; the password is prompted.
`.env` already holds `SUMMER_ADMIN__JWT__SECRET`, which signs admin tokens.
## Naming convention
- `sm-<name>-app`: an application, like this repository.
@@ -143,4 +160,11 @@ and summer plugin:add plugins/<name>, then run summer build again.
{{else}}
Then, from another terminal:
curl http://127.0.0.1:8080/
{{end}}{{end}}
{{end}}
Admin, at http://127.0.0.1:8080/backend once a plugin registers an admin
controller (.env already holds SUMMER_ADMIN__JWT__SECRET):
summer make:admin-controller golem15.<plugin> <Name>
summer build
summer migrate
summer admin:create --email you@example.com --superuser
{{end}}

View File

@@ -23,6 +23,8 @@ summer migrate # run plugin migrations
summer serve # start the HTTP server
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
```
## Layout
@@ -30,13 +32,23 @@ summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.w
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
- `plugins/`: local plugin modules.
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
- `.env`: local secrets (database DSN, application key). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `compose.yaml`: the local PostgreSQL 16.
- `main.go`, `plugins.gen.go`: generated by `summer build` from `summer.yaml`. Do not edit them by hand; they are committed.
- `go.work`: the workspace that joins the application and its plugin modules.
Plugins are Go modules compiled into the binary at build time; there is no runtime plugin loading. Add a plugin with `summer make:plugin` and `summer plugin:add`, then run `summer build`.
## Admin
The backend admin (the admin SPA and its JSON API) lives under `/backend`, at `http://127.0.0.1:8080/backend` with the default `summer serve` address. It is mounted only when a plugin registers an admin controller; until then `summer serve` prints `admin: off`. To add it:
1. `summer make:admin-controller golem15.<plugin> <Name>` scaffolds the controller and its YAML. The generated controller requires the permission `golem15.<plugin>.access_<name>` (`<name>` is the controller name in snake case): declare it in the plugin's `Permissions()` (`pact.HasPermissions`) or the admin refuses to start. Its `NewRecord` returns `nil` until you return the plugin's model.
2. `summer build`, then `summer migrate`.
3. `summer admin:create --email you@example.com --superuser` creates the first administrator; the password is prompted.
`.env` already holds `SUMMER_ADMIN__JWT__SECRET`, which signs admin tokens.
## Naming convention
- `sm-<name>-app`: an application, like this repository.

View File

@@ -8,3 +8,10 @@ Next steps, inside sm-demo-app:
The app has no plugins yet. Add one with summer make:plugin golem15.<name>
and summer plugin:add plugins/<name>, then run summer build again.
Admin, at http://127.0.0.1:8080/backend once a plugin registers an admin
controller (.env already holds SUMMER_ADMIN__JWT__SECRET):
summer make:admin-controller golem15.<plugin> <Name>
summer build
summer migrate
summer admin:create --email you@example.com --superuser

View File

@@ -23,6 +23,8 @@ summer migrate # run plugin migrations
summer serve # start the HTTP server
summer make:plugin golem15.<name> # scaffold a plugin module under plugins/<name>
summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.work
summer make:admin-controller golem15.<plugin> <Name> # scaffold an admin controller with its YAML
summer admin:create --email you@example.com --superuser # create a backend administrator (password prompted)
```
## Layout
@@ -30,13 +32,23 @@ summer plugin:add plugins/<name> # register it in summer.yaml, go.mod and go.w
- `summer.yaml`: the application manifest (module path, binary name, ordered plugin list).
- `plugins/`: local plugin modules. `plugins/demo` is the starter plugin `golem15.demo`; its `routes.go` answers `GET /` with JSON.
- `config/`: YAML configuration (`app.yaml`, `database.yaml`, `http.yaml`, `storage.yaml`). Any key can be overridden with a `SUMMER_` environment variable, a double underscore separating path segments (`SUMMER_DATABASE__DSN` sets `database.dsn`).
- `.env`: local secrets (database DSN, application key). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `.env`: local secrets (database DSN, application key, admin token secret). Never commit it; `.gitignore` excludes it. `.env.example` lists the variables.
- `compose.yaml`: the local PostgreSQL 16.
- `main.go`, `plugins.gen.go`: generated by `summer build` from `summer.yaml`. Do not edit them by hand; they are committed.
- `go.work`: the workspace that joins the application and its plugin modules.
Plugins are Go modules compiled into the binary at build time; there is no runtime plugin loading. Add a plugin with `summer make:plugin` and `summer plugin:add`, then run `summer build`.
## Admin
The backend admin (the admin SPA and its JSON API) lives under `/backend`, at `http://127.0.0.1:8080/backend` with the default `summer serve` address. It is mounted only when a plugin registers an admin controller; until then `summer serve` prints `admin: off`. To add it:
1. `summer make:admin-controller golem15.<plugin> <Name>` scaffolds the controller and its YAML. The generated controller requires the permission `golem15.<plugin>.access_<name>` (`<name>` is the controller name in snake case): declare it in the plugin's `Permissions()` (`pact.HasPermissions`) or the admin refuses to start. Its `NewRecord` returns `nil` until you return the plugin's model.
2. `summer build`, then `summer migrate`.
3. `summer admin:create --email you@example.com --superuser` creates the first administrator; the password is prompted.
`.env` already holds `SUMMER_ADMIN__JWT__SECRET`, which signs admin tokens.
## Naming convention
- `sm-<name>-app`: an application, like this repository.

View File

@@ -4,6 +4,10 @@ SUMMER_DATABASE__DSN=postgres://summer:summer@127.0.0.1:54320/demo?sslmode=disab
# A 32-byte base64 application key. Generate one with ./bin/demo key:generate
# and paste only the key, without the leading check mark.
SUMMER_APP__KEY=
# Signs backend admin tokens; the admin refuses to start without it once a
# plugin registers an admin controller. summer new wrote a random value to .env;
# any long random string works, for example one from ./bin/demo key:generate.
SUMMER_ADMIN__JWT__SECRET=
# Uploads bucket; config/storage.yaml already sets this default.
# SUMMER_STORAGE__UPLOADS__BUCKET_URL=file://./storage/app/uploads
# Configuration environment; selects the config overlay (default production).

View File

@@ -1,5 +1,6 @@
# Local secrets for this application: the database DSN and the application
# key. .gitignore excludes this file; never commit it. Real environment
# variables override the values here.
# Local secrets for this application: the database DSN, the application key
# and the admin token secret. .gitignore excludes this file; never commit it.
# Real environment variables override the values here.
SUMMER_DATABASE__DSN=postgres://summer:summer@127.0.0.1:54320/demo?sslmode=disable
SUMMER_APP__KEY=AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
SUMMER_ADMIN__JWT__SECRET=ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=

View File

@@ -8,3 +8,10 @@ Next steps, inside sm-demo-app:
Then, from another terminal:
curl http://127.0.0.1:8080/
Admin, at http://127.0.0.1:8080/backend once a plugin registers an admin
controller (.env already holds SUMMER_ADMIN__JWT__SECRET):
summer make:admin-controller golem15.<plugin> <Name>
summer build
summer migrate
summer admin:create --email you@example.com --superuser