diff --git a/README.md b/README.md index e9bae54a..564e2bb7 100644 --- a/README.md +++ b/README.md @@ -1,180 +1,170 @@ # GoNext -A modern, modular content management platform. WordPress's ecosystem promise, built on Go + Next.js + Postgres, with a sandboxed plugin runtime so you can actually trust what you install. +> A self-hosted WordPress alternative built on Go + Next.js. -> **Status**: Pre-1.0. ~137 PRs landed; the platform boots, serves a themed page, and now ships a first-run setup wizard. Contributors welcome. +GoNext gives you WordPress's "own your content, install plugins, switch themes" model on a modern stack — a single Go binary serves the API and a WebAssembly plugin runtime; two Next.js apps render the public site and the admin dashboard; Postgres + Redis + S3-compatible storage sit underneath. The plugin sandbox is capability-scoped and memory-isolated, so you can install something from the marketplace without praying it doesn't read `/etc/passwd`. -## What this is +> **Status**: pre-1.0. The stack boots end-to-end, the first-run install wizard works, posts render through the themed public site, and the marketplace + setup flows are wired. We tag releases when things land; pin to a tag if you're shipping. -- **Backend**: a single Go binary (HTTP server + WebAssembly plugin host + background workers). -- **Frontend**: Next.js for the public site (SSR/SSG/ISR) and a separate Next.js app for the admin. -- **Storage**: PostgreSQL + Redis + S3-compatible object storage. -- **Plugins**: WebAssembly modules with a capability-based ABI. Memory-isolated. Signed. -- **Themes**: React component packages. Both classic (code-defined templates) and block themes (full-site editing). +![Sign-in screen of the GoNext admin dashboard. Cream paper background with soft off-canvas emerald and lavender radial glows. A centered card holds the `GoNext` wordmark (see apps/admin/public/logo-wordmark.svg), an Archivo display headline reading "Sign in" with an italic accent on "in", a Geist body-copy subtitle, and email + password inputs over a primary "Sign in" button.](docs/design/screenshots/login.png) -## What this is NOT +*Screenshot pending — the brand foundation landed in #432 and a follow-up will capture the asset. Until then, the wordmark lives at `apps/admin/public/logo-wordmark.svg`.* -- Not a WordPress fork. Not PHP. Will not run WordPress plugins. **Does** provide tools to import WordPress content. -- Not a headless-only CMS. The admin and editor are first-class. -- Not feature parity with every WordPress feature. We aim at the 95% of common use cases, done well. +--- -## Quickstart +## The first 10 minutes -You have two paths to a working install. Pick one. +You'll need Docker Desktop ≥ 24 (with Compose v2 — `docker compose`, not `docker-compose`) and GNU Make. Nothing else; no Go or Node on your host. -### Path A — Browser (the WordPress route) +```bash +# 1. Clone. +git clone https://github.com/Singleton-Solution/GoNext.git +cd GoNext -The fastest way to a usable site. Mirrors WordPress's `/wp-admin/install.php`. +# 2. Copy the env template. Edit secrets before you ship to anything +# that isn't your laptop — the file ships with dev-only defaults +# flagged "replace-in-prod". +cp .env.example .env -```sh -# 1. Bring up Postgres + Redis + MinIO. +# 3. Bring up the full stack: Postgres, Redis, MinIO, migrate (one-shot), +# api, worker, admin, web. `make up` composes the base +# docker-compose.yml with docker-compose.dev.yml — see the local-dev +# doc for the full long-form command. make up -# 2. Apply migrations (one shot — idempotent on subsequent runs). -make build-go && ./apps/api/bin/gonext migrate up +# 4. Bootstrap the first admin user. Two options — pick one. -# 3. Start the API and admin. -# In two terminals (or use your preferred process manager): -go run ./apps/api/cmd/server # API on :8080 -pnpm --filter @gonext/admin dev # Admin on :3001 +# Option A (CLI): scripted, friendly to CI and air-gapped boxes. +docker compose run --rm migrate \ + gonext init \ + --admin-email you@example.com \ + --admin-password 'replace-with-a-real-≥12-char-password' \ + --site-name 'My Site' \ + --site-url http://localhost:3000 + +# Option B (browser): the WordPress-style wizard. Open +# http://localhost:3001/setup and walk through welcome → admin +# credentials → site name + URL → confirm. The wizard locks itself +# on success — every /api/v1/setup/* endpoint returns 423 Locked +# afterwards, and the admin middleware stops redirecting to /setup. + +# 5. Sign in. +open http://localhost:3001/login ``` -Then open **http://localhost:3001/setup** in your browser. The setup wizard walks you through: +That's it. The public site is at `http://localhost:3000`, the API at `http://localhost:8080`, the MinIO console at `http://localhost:9001` (creds: `gonext` / `gonext_dev_only_change_me`). -1. Welcome + system check -2. Administrator email + password (≥12 characters, enforced server-side) -3. Site name + URL -4. Review + confirm -5. Auto-redirect into the admin dashboard, already logged in +To stop everything (volumes preserved): `make down`. To wipe state and start over: `docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v && make up`. -After the wizard succeeds the `/setup` route is permanently locked — every endpoint under `/api/v1/setup/*` returns `423 Locked` and the admin middleware stops redirecting to it. You can re-open the install window only by dropping the `core.site.installation_completed_at` row from the `options` table (psql, out-of-band). +--- -> *Screenshot of the wizard goes here once the design system lands.* +## Local development tips -### Path B — CLI (the scripted route) +A few things that bit us when we ran this for the first time — fix them up front and the stack just works. -For deployments where the admin UI isn't reachable from the operator's workstation (CI, Kubernetes init container, air-gapped bootstrap): +### Port conflicts: the `docker-compose.override.yml` pattern -```sh -make up -./apps/api/bin/gonext migrate up -./apps/api/bin/gonext init \ - --admin-email=admin@example.com \ - --admin-password='correct-horse-battery-staple' \ - --site-name='Acme CMS' \ - --site-url=https://acme.example.com +The dev stack publishes Postgres on `5432`, Redis on `6379`, the API on `8080`, the admin on `3001`, the public site on `3000`, MinIO on `9000`/`9001`. If you already run a native Postgres for another project, `make up` fails with `Bind for 0.0.0.0:5432 failed: port is already allocated`. + +Fix it once, in a personal override file that Compose merges automatically and that git already ignores: + +```yaml +# docker-compose.override.yml (gitignored) +services: + postgres: + ports: + - "5433:5432" # talk to the dev DB on 5433 from the host ``` -The CLI hits the same `POST /api/v1/setup/install` endpoint the wizard uses, so the lock behavior is identical — a second invocation returns the same `423 already_installed` code. +Then connect your IDE / `psql` / `make psql` to `localhost:5433` instead of `5432`. The same trick works for any other published port. Compose reads `docker-compose.override.yml` without you having to name it on the command line, which is exactly what you want — your override stays personal and local. -> The `gonext init` subcommand is scaffolded but not yet shipped; track its delivery in [issue #TBD]. Until then, use Path A. +### The admin proxies `/api/*` through Next.js to avoid CORS -## Where to go next +The admin (`:3001`) and the API (`:8080`) are different origins during dev. Rather than ship a CORS allowlist that has to be re-derived for every deploy shape, the admin's `next.config.ts` rewrites `/api/:path*` to the API service. Two consequences: -- `docs/00-architecture-overview.md` — the foundation. Read first. -- `docs/06-auth-permissions.md` — argon2id, sessions, roles, the setup wizard's security model. -- `docs/09-deployment-ops.md` — Docker, Kubernetes, env vars, multi-region. -- `docs/13-security-baseline.md` — CSP, secret handling, supply-chain posture. -- `docs/11-testing-ci.md` — running the test pyramid locally. +- **`NEXT_PUBLIC_API_URL=""` is the signal**, not a missing value. The api-client treats an empty string as "use same-origin paths" and lets the rewrite do the work. Setting it to `http://localhost:8080` in dev means the browser hits the API directly, CORS fires, and you spend a confusing afternoon staring at preflight failures. Leave it empty unless you're deliberately testing the cross-origin path. +- **The rewrite destination is baked at build time.** Next.js evaluates `rewrites()` during `next build`, so the container image has a fixed `http://api:8080` (the Compose service name) compiled into the bundle. Pass the destination as the `GONEXT_API_URL` Docker build-arg if you're building a custom image; the default Dockerfile reads it via `NEXT_PUBLIC_API_URL`. -Proposals for all open questions live in [`/docs/proposals`](./docs/proposals). -Architecture Decision Records in [`/adr`](./adr). +```dockerfile +# Admin image build (from docker-compose.dev.yml) +args: + NEXT_PUBLIC_API_URL: http://api:8080 # destination for the rewrite +``` -## Quickstart +### Secret alignment -```sh -# 1. Copy the sample env file and edit the secrets. -cp .env.example .env +`GONEXT_AUTH_PEPPER` is HMAC'd into every password hash on user creation. If you re-bootstrap with a different pepper and the same database, every existing user's password becomes uncrackable — `init` will succeed, but you can't log back in as the previous admin. Either set the pepper once and keep it stable, or wipe the DB volume (`down -v`) when you rotate it. -# 2. Generate the three required auth secrets. -openssl rand -base64 32 # paste into GONEXT_AUTH_PEPPER -openssl rand -base64 32 # paste into GONEXT_AUTH_SESSION_SECRET -openssl rand -base64 32 # paste into GONEXT_AUTH_CSRF_SECRET +### Other gotchas -# 3. Bring up Postgres + Redis + MinIO and the API. -docker compose up -``` +For a fuller list — Next.js prerender failures on `useSearchParams` missing `Suspense`, the migrate one-shot exiting `dirty` after a half-applied schema, MinIO bucket creation timing — see [`docs/20-troubleshooting.md`](docs/20-troubleshooting.md). -Every environment variable the API reads is documented in [`.env.example`](.env.example) with default, type, and security notes. For the prose reference (per-section tables, redaction rules, K8s / systemd deployment shapes), see [`docs/17-environment.md`](docs/17-environment.md). -A fresh checkout becomes a working site in three commands. You'll need Postgres reachable via `DATABASE_URL` and a pepper secret (any high-entropy string) in `GONEXT_AUTH_PEPPER`. +--- -```bash -# 1. Build the CLI. -go build -o ./bin/gonext ./cli/gonext - -# 2. First-run bootstrap: applies migrations, installs the default -# theme, creates the initial super_admin user, and stamps the -# site name + URL into the options table. Re-running on an -# already-initialized install is a no-op. -DATABASE_URL='postgres://gonext:gonext@localhost:5432/gonext?sslmode=disable' \ -GONEXT_AUTH_PEPPER='replace-me-with-a-real-secret' \ - ./bin/gonext init \ - --admin-email you@example.com \ - --site-name 'My Site' \ - --site-url https://example.com +## What's where -# 3. Run the API + worker (see apps/api, apps/worker). -``` +| Path | What lives there | +| --- | --- | +| `apps/api` | Go HTTP server. `/healthz`, `/readyz`, `/openapi.json`, `/docs/`, every `/api/v1/*` route. | +| `apps/worker` | Asynq background-job consumer. Image processing, webhooks, cron leaders. | +| `apps/admin` | Next.js admin dashboard. Login, setup wizard, posts/pages CRUD, marketplace, customizer. | +| `apps/web` | Next.js public site. SSR/SSG/ISR, themes, sitemap, feeds. | +| `apps/docs` | Static documentation site (deploys separately from the app). | +| `cli/gonext` | The `gonext` administrative CLI. `init`, `migrate`, `theme`, `plugin`, `bench`, `config`. | +| `packages/go` | Shared Go packages — auth, config, log, db, cache, hooks, middleware, testutil, etc. | +| `packages/ts` | Shared TypeScript packages — UI primitives, block schemas, the plugin/theme SDKs. | +| `migrations` | `golang-migrate` SQL files. Applied by `gonext migrate up` or the Compose `migrate` one-shot. | +| `themes` | First-party theme bundles (the default theme is seeded by `migrate`). | +| `plugins` | First-party reference plugins (WASM bundles + manifests). | +| `docs` | The whole design corpus — architecture docs 00–19, ADRs, proposals, troubleshooting. | +| `tools` | One-off operator tooling: the compose smoke harness, the e2e Playwright suite. | -Pass `--admin-password` (insecure on shared hosts), `--admin-password-stdin` (pipe from a secret manager), or omit both and `init` will prompt with no-echo. Add `--non-interactive` in CI to fail fast on missing fields. The full flag list is in `gonext init --help`. - -## Design documents - -| # | Document | What it covers | -|---|---|---| -| 00 | [Architecture Overview](docs/00-architecture-overview.md) | Foundation. Read first. | -| 01 | [Core CMS & Data Model](docs/01-core-cms.md) | Content types, taxonomies, Postgres schema | -| 02 | [Plugin System](docs/02-plugin-system.md) | WASM runtime, hook bus, capability ABI | -| 03 | [Theme System](docs/03-theme-system.md) | Template hierarchy, theme.json, FSE | -| 04 | [Block Editor](docs/04-block-editor.md) | JSON block tree, editor UX | -| 05 | [Admin & API](docs/05-admin-api.md) | Admin UI, REST + GraphQL | -| 06 | [Auth & Permissions](docs/06-auth-permissions.md) | Argon2id, sessions, roles & capabilities | -| 07 | [Media & Performance](docs/07-media-performance.md) | Upload pipeline, cache layers, ISR | -| 08 | [Migration & WP Compat](docs/08-migration-compat.md) | WordPress importers, REST shim | -| 09 | [Deployment & Ops](docs/09-deployment-ops.md) | Docker, K8s, env, multi-region | -| 10 | [Observability](docs/10-observability.md) | Logs, metrics, traces, RUM | -| 11 | [Testing & CI](docs/11-testing-ci.md) | Test pyramid, contract tests, CI | -| 12 | [Jobs & Cron](docs/12-jobs-cron.md) | Asynq queues, retries, leader election | -| 13 | [Security Baseline](docs/13-security-baseline.md) | Headers, CSP, secrets, supply chain | -| 17 | [Environment & Configuration](docs/17-environment.md) | Every env var the loader reads — type, default, deployment patterns | - -Proposals for all open questions live in [`/docs/proposals`](./docs/proposals). -Architecture Decision Records in [`/adr`](./adr). -| 15 | [Security Policy](docs/15-security-policy.md) | Vulnerability disclosure, SLA | -| 16 | [Bug Bounty](docs/16-bug-bounty.md) | Scope, rewards | - -## Roadmap - -Six phases, ~24 months to v1 with two engineers. See [ROADMAP.md](./ROADMAP.md) for detail. - -| Phase | Scope | Milestone | -|---|---|---| -| P0 | Skeleton | Go server, schema, basic auth, one rendered page | -| P1 | CMS Core | Posts/pages/CPTs, taxonomies, media, admin CRUD | -| P2 | Editor | Block editor with ~20 core blocks | -| P3 | Themes | Template hierarchy, customizer, 1-2 reference themes | -| P4 | Plugins | WASM runtime, SDK, 3 reference plugins | -| P5 | Migration | WordPress importer, REST compat | -| P6 | Polish | Performance, docs, launch | +--- -## Contributing +## Documentation map + +If you want the next layer down, read these in order. -We need help. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to pick up an issue. +| Document | What it covers | +| --- | --- | +| [`docs/00-architecture-overview.md`](docs/00-architecture-overview.md) | The shared foundation. Stack, the three hard problems, the topology, the phasing plan. **Read first.** | +| [`docs/17-environment.md`](docs/17-environment.md) | Every env var the loader reads — type, default, redaction rules, K8s + systemd shapes. | +| [`docs/18-local-development.md`](docs/18-local-development.md) | The dev-stack reference: Make targets, the override pattern, the smoke harness, troubleshooting. | +| [`docs/20-troubleshooting.md`](docs/20-troubleshooting.md) | Symptom → cause → fix for every gotcha we hit running the stack for the first time. | -- Browse [open issues](https://github.com/Singleton-Solution/GoNext/issues) filtered by `area:*`, `skill:*`, or `good-first-issue`. -- Read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). -- Sign off your commits with `git commit -s` (the [DCO](https://developercertificate.org/) check enforces this on PRs — see [CONTRIBUTING.md](./CONTRIBUTING.md#dco-sign-off)). +The full catalogue (auth, plugin system, theme system, block editor, observability, jobs, security baseline, etc.) lives in [`docs/README.md`](docs/README.md). Architecture Decision Records are under [`adr/`](adr/). Open design proposals are under [`docs/proposals/`](docs/proposals/). + +--- ## License -License is being finalized — see [`LICENSE`](./LICENSE) and the rationale in [`docs/proposals/14-proposals-strategic.md`](./docs/proposals/14-proposals-strategic.md) §S2. +GoNext core is licensed under **FSL-1.1-Apache-2.0** — the Functional Source License 1.1 with automatic conversion to Apache License 2.0 two years after each file's release. Source-available today, fully open-source on a two-year delay. Read it: [`LICENSE`](LICENSE). + +The plugin and theme SDKs (everything under `packages/ts/sdk` and `packages/go/sdk`) ship under **Apache-2.0** from day one, so authors building on top of GoNext have a permissive license to work against without any FSL strings attached. + +The rationale, including why we picked FSL over BSL or MIT, lives in [`adr/0001-licensing.md`](adr/0001-licensing.md). -Current direction: **core under FSL-1.1-Apache-2.0** (source-available, converts to Apache 2.0 after 2 years per file) with the **plugin SDK under Apache 2.0** from day 1. Contributors sign off commits via the [DCO](https://developercertificate.org/) (no CLA). See [`adr/0001-licensing.md`](./adr/0001-licensing.md) and [`adr/0002-dco-requirement.md`](./adr/0002-dco-requirement.md). +--- + +## Contributing + +We need help. Go developers, React developers, designers, technical writers, security reviewers — there's an issue with your name on it. + +1. **Find an issue.** Filter the [issue tracker](https://github.com/Singleton-Solution/GoNext/issues) by `good-first-issue`, `help-wanted`, `area:*` (api, web, admin, plugins, themes, security, docs, …), or `skill:*` (go, react, ts, sql, devops, design, docs). Comment on the issue to claim it. +2. **Branch.** `git checkout -b feat/` or `fix/` off `main`. +3. **Sign off every commit.** GoNext uses the [Developer Certificate of Origin](https://developercertificate.org/) instead of a CLA. Pass `-s` to `git commit` — the CI check rejects unsigned commits. See [`adr/0002-dco-requirement.md`](adr/0002-dco-requirement.md) for the rationale and [`CONTRIBUTING.md`](CONTRIBUTING.md) for the full workflow. +4. **Open the PR against `main`.** Reference the issue you're closing. Keep PRs small and focused — one logical change per PR. + +[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) applies to every interaction in the repo. + +--- ## Security -Report vulnerabilities privately per [SECURITY.md](./SECURITY.md). Do not file public issues for security reports. +Report vulnerabilities privately per [`SECURITY.md`](SECURITY.md). Do not file public issues for security reports. + +--- ## Maintainer -Currently maintained by [@tayebmokni](https://github.com/tayebmokni) under [Singleton-Solution](https://github.com/Singleton-Solution). Project governance will transition to a maintainer team as the contributor base grows; see [GOVERNANCE.md](./GOVERNANCE.md) (coming). +Currently maintained by [@tayebmokni](https://github.com/tayebmokni) under [Singleton-Solution](https://github.com/Singleton-Solution). Governance transitions to a maintainer team as the contributor base grows. diff --git a/docs/18-local-development.md b/docs/18-local-development.md index e1c98db6..9cdd6a07 100644 --- a/docs/18-local-development.md +++ b/docs/18-local-development.md @@ -90,6 +90,102 @@ The dev secrets are intentionally low-entropy and labelled `replace-in-prod` — production deploys MUST source secrets from a secrets manager. See [13-security-baseline.md §5](./13-security-baseline.md). +## The `docker-compose.override.yml` pattern + +Docker Compose has a convention that's perfect for personal local +tweaks: any file named `docker-compose.override.yml` next to the base +compose file is merged in automatically, with no extra `-f` flag, and +the path is already in `.gitignore`. Use it for anything you want to +change that isn't worth a tracked-file commit. + +The single most common need is **remapping a published port** when +something on your host is already on the same number. The dev stack +publishes: + +| Service | Container port | Host port | +|----------|----------------|-------------| +| postgres | 5432 | 5432 | +| redis | 6379 | 6379 | +| minio | 9000 / 9001 | 9000 / 9001 | +| api | 8080 | 8080 | +| admin | 3000 | 3001 | +| web | 3000 | 3000 | + +If you already run a native Postgres on 5432, drop this into +`docker-compose.override.yml`: + +```yaml +services: + postgres: + ports: + - "5433:5432" # publish on 5433 from the host; container stays on 5432 +``` + +Now `make psql` (which talks via `docker compose exec`) still works +unchanged, but your IDE, `psql`, `pgcli`, and any host-local tooling +should point at `localhost:5433`. The same pattern fixes conflicts on +any other published port — only the `services:` map matters; the rest +of the base compose file is unchanged. + +Other things people commonly drop into the override file: + +```yaml +services: + api: + environment: + # Crank up the logs while debugging a specific issue. + GONEXT_LOG_LEVEL: DEBUG + GONEXT_LOG_ADDSRC: "true" + admin: + volumes: + # Mount your local app source for hot-reload outside Compose. + - ./apps/admin/src:/app/apps/admin/src +``` + +## How the admin reaches the API: Next.js rewrites, not CORS + +The admin (Next.js, served on `:3001` from the host) and the API +(`:8080`) are different origins during dev. Two ways to bridge that: + +1. **CORS**: ship an `Access-Control-Allow-Origin` allowlist on the API + that matches every shape a deployment might take. +2. **Same-origin proxy**: rewrite `/api/:path*` on the admin server to + the API service, so the browser only ever talks to `:3001`. + +We picked option 2. The admin's `next.config.ts` defines a `rewrites()` +block that forwards `/api/*` to the API. The browser sees one origin; +no preflight; no allowlist to maintain. + +Two consequences flow from that choice: + +- **`NEXT_PUBLIC_API_URL=""` is the signal**, not a missing value. + `apps/admin/src/lib/api-client.ts` treats an empty string as "use + same-origin paths" — `/api/v1/posts` etc. — and lets the rewrite do + the work. Setting `NEXT_PUBLIC_API_URL=http://localhost:8080` makes + the browser hit the API directly, which means CORS preflights, which + means an afternoon staring at network-tab errors. Leave it empty + unless you're deliberately testing the cross-origin path. + +- **The rewrite destination is baked in at build time.** Next.js + evaluates `rewrites()` during `next build`, so the container image + has a fixed destination compiled into the bundle. The Dockerfile + declares `ARG NEXT_PUBLIC_API_URL=""` and the Compose build block + passes `http://api:8080` — the cluster-internal service name — as + the build-arg. If you build a custom admin image you need to pass + the same arg pointed at whatever your API is reachable as inside the + cluster (e.g. `--build-arg NEXT_PUBLIC_API_URL=http://api:8080` for + Compose, or your K8s Service DNS name in a cluster build). + + In some deployments we pass this through as `GONEXT_API_URL` so the + variable name reflects "the GoNext API target", not "the public env + var Next bakes into the bundle"; the Dockerfile's `ARG` block is the + single point that ties the two names together. + +The rewrite happens at the Next.js server (port `:3000` inside the +admin container, `:3001` from the host) — *not* in the browser. So +the API service only needs to accept connections from the admin +container on the Compose network, not from `localhost:3001`. + ## The smoke harness `make smoke` invokes `tools/compose-smoke/compose-smoke.sh`. The script: @@ -122,11 +218,16 @@ HEALTH_TIMEOUT_SECS=120 make smoke # raise the per-probe budget ## Troubleshooting +For a wider symptom → cause → fix catalogue, see +[20-troubleshooting.md](./20-troubleshooting.md). The list below covers +just the issues that surface during day-one local-dev setup. + **"Bind for 0.0.0.0:8080 failed: port is already allocated"** — something else on your host is already on 8080, 3000, 3001, 5432, 6379, 9000, or 9001. Either stop the conflicting container (`docker ps`, then `docker stop `) or remap the published port in -a personal `docker-compose.override.yml`. +a personal `docker-compose.override.yml` (see the section above for +the canonical Postgres-on-5433 example). **"service \"migrate\" didn't complete successfully: exit 1"** — look at `make logs` for the `migrate-1` container. The most common @@ -146,6 +247,30 @@ care about. Redis. `make ps` will show whether those containers are healthy; `make logs` shows the api binary's connection error. +**Admin login: "invalid email or password" with the right +credentials** — almost always a pepper mismatch. `GONEXT_AUTH_PEPPER` +is HMAC'd into every password hash; rotating it without rehashing +existing rows makes every existing password uncrackable. Either keep +the pepper stable across re-bootstraps, or wipe state with +`make down && docker volume rm gonext-dev_postgres-data && make up` +and rerun `gonext init`. + +**Admin build fails with `useSearchParams() should be wrapped in a +suspense boundary`** — Next.js 15 requires any client component that +reads `useSearchParams()` from the App Router to sit inside a +`` boundary, because the hook reads dynamic data that +isn't available at prerender time. If you add a new page using the +hook, wrap the consumer in ``. The compile +error points at the file path; the fix is local. + +**`make up` succeeds but admin returns 502 / network error** — the +admin built with `NEXT_PUBLIC_API_URL` pointing at something +unreachable. Check the Compose build args (should be +`http://api:8080`) and confirm the api service is actually healthy via +`make ps`. If you've been switching between Compose and bare +`pnpm dev` runs, the cached `.next/` build may have the wrong baked +destination — `rm -rf apps/admin/.next` and rebuild. + **Slow rebuilds** — the multi-stage Dockerfiles use BuildKit cache mounts for the Go module cache and the pnpm store. Touch a Go file and rebuild — the second run should be sub-30s. If it isn't, your diff --git a/docs/20-troubleshooting.md b/docs/20-troubleshooting.md new file mode 100644 index 00000000..b3823470 --- /dev/null +++ b/docs/20-troubleshooting.md @@ -0,0 +1,322 @@ +# 20 · Troubleshooting + +A symptom → cause → fix catalogue for the issues that bit us actually +running the stack. Skim the symptoms; jump to the one that matches. +For per-subsystem deep dives (auth, plugins, themes, jobs) go to the +relevant numbered doc — this file is the shallow-but-broad +operator-facing layer. + +If your problem isn't here, please open an issue. Real first-run +friction is the kind of bug we most want to learn about. + +--- + +## 1. Stack won't start + +### 1.1 `Bind for 0.0.0.0: failed: port is already allocated` + +**Symptom.** `make up` exits with a port-conflict error. The port is +one of 5432 (postgres), 6379 (redis), 8080 (api), 3000 (web), 3001 +(admin), 9000 / 9001 (MinIO). + +**Cause.** Another process on your host (often a native Postgres, a +side-project's Compose stack, or a stray container) already publishes +the same port. + +**Fix.** Drop a `docker-compose.override.yml` in the repo root that +remaps the conflicting port: + +```yaml +services: + postgres: + ports: + - "5433:5432" +``` + +Docker Compose merges the override automatically; the file is already +gitignored. Then point host-side tooling at the new port +(`localhost:5433` in this example). See +[18-local-development.md](./18-local-development.md#the-docker-composeoverrideyml-pattern) +for the long-form explanation. + +### 1.2 `service "migrate" didn't complete successfully: exit 1` + +**Symptom.** The Compose stack starts; the `migrate` one-shot fails +before api / worker boot. `make ps` shows the data services healthy +but the apps stuck in `Created`. + +**Cause.** The `schema_migrations` table is in a `dirty` state. Usually +this is the residue of a previous run that was interrupted mid-DDL — +the migration partially applied, the runner crashed, the row was left +flagged dirty. + +**Fix.** + +```bash +make down +docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v +make up +``` + +`down -v` wipes the Postgres / Redis / MinIO data volumes. Fine on +your laptop; never on a database that holds anything you care about. + +### 1.3 `api` boots but `/readyz` returns 503 + +**Symptom.** `make ps` shows `api` running; `curl localhost:8080/readyz` +returns 503. `/healthz` returns 200. + +**Cause.** The API process started, but its DB or Redis probe is +failing. `/readyz` is the conjunction of "DB reachable" AND "Redis +reachable"; `/healthz` is just "process alive". + +**Fix.** Check `make ps` for unhealthy data services. Check `make logs` +for `connect: connection refused` or `dial tcp: lookup postgres on … +no such host` on the API container — both point at network or +healthcheck-ordering issues. Bring the stack down and back up; if it +persists, post the api logs. + +--- + +## 2. First-run bootstrap + +### 2.1 `gonext init` succeeds but you can't log in afterwards + +**Symptom.** `gonext init --admin-email …` reports success. You open +`/login`, enter the email + password you just set, and get "invalid +email or password". + +**Cause.** Pepper mismatch. `GONEXT_AUTH_PEPPER` is HMAC'd into every +password hash. If the pepper used during `init` differs from the +pepper the api server reads at request time, the hash never matches +and login fails. The most common path here is running `init` outside +Compose (which reads a different env) and `make up` inside Compose +(which has dev defaults baked into `docker-compose.dev.yml`). + +**Fix.** Make sure both processes read the same pepper. Either: + +- Run `init` via Compose so it inherits the same `x-go-env` block: + `docker compose run --rm migrate gonext init …`. +- Or set `GONEXT_AUTH_PEPPER` in your shell to the value in + `docker-compose.dev.yml` before running `init` against the dev DB. + +If the wrong pepper is already committed to a row, the only recovery +is to wipe and redo: `make down && docker compose -f +docker-compose.yml -f docker-compose.dev.yml down -v && make up && +docker compose run --rm migrate gonext init …`. + +### 2.2 The `/setup` wizard 423s before you've finished + +**Symptom.** You open `/setup`, fill in the form, hit submit — and the +api returns `423 already_installed`. + +**Cause.** Installation is single-shot. The `core.site.installation_completed_at` +row in `options` records the install timestamp; once present, every +`POST /api/v1/setup/*` returns 423 and the admin middleware stops +redirecting to `/setup`. You're seeing this because a previous run +already installed. + +**Fix.** + +- *Intentional re-install* (you really want to start over): drop the + row out of band. From `make psql`: + + ```sql + DELETE FROM options WHERE name = 'core.site.installation_completed_at'; + ``` + + Then reload `/setup`. The wizard will re-open. + +- *Just trying to log in*: the install already succeeded — go to + `/login`. The wizard finished its job. + +### 2.3 `gonext init` errors with "admin already exists" + +**Symptom.** `init` exits 1 with a message about the admin user +already being present. + +**Cause.** `init` is intentionally idempotent on the *schema* layer +(re-running just no-ops the migrations), but refuses to silently +clobber an existing user. If a previous run created the same email +address, it bails out so you don't accidentally overwrite a real +user's credentials. + +**Fix.** Either pick a different email (`--admin-email`) or wipe the DB +volume and start fresh. + +--- + +## 3. Admin app + +### 3.1 Admin shows "network error" / `502` on every page + +**Symptom.** The admin loads, but every panel hits a network error. +Network tab shows requests to `/api/v1/...` failing. + +**Cause.** Two flavours: + +1. The admin was built with `NEXT_PUBLIC_API_URL` pointing somewhere + unreachable. The rewrite destination is baked at build time + (see §3.2), so a wrong build-arg produces a broken image. +2. The api service isn't healthy. + +**Fix.** Check `make ps` first — if the api isn't healthy, fix that. +Otherwise rebuild the admin with the right build-arg: + +```bash +docker compose -f docker-compose.yml -f docker-compose.dev.yml \ + build --build-arg NEXT_PUBLIC_API_URL=http://api:8080 admin +make up +``` + +If you switched between Compose and bare `pnpm dev`, blow away the +stale `.next/` cache: `rm -rf apps/admin/.next`. + +### 3.2 Why `NEXT_PUBLIC_API_URL=""` is the right value in dev + +**Symptom.** You set `NEXT_PUBLIC_API_URL=http://localhost:8080` to +"fix" something, and now every admin request gets a CORS preflight +failure. + +**Cause.** The admin's `next.config.ts` declares a `rewrites()` block +that proxies `/api/:path*` to the API service over the Compose +network. The api-client treats an empty `NEXT_PUBLIC_API_URL` as "use +same-origin paths" so the rewrite catches them; an explicit +`http://localhost:8080` makes the browser try to reach the API +directly, which triggers CORS. + +**Fix.** Set `NEXT_PUBLIC_API_URL=""` (the default in the Dockerfile +and the Compose env). For custom builds, leave it empty unless you +genuinely want the browser to talk cross-origin to the API. + +### 3.3 `useSearchParams() should be wrapped in a suspense boundary` + +**Symptom.** `next build` fails with the message above and a path to a +page file under `apps/admin/src/app/...`. + +**Cause.** Next.js 15 requires any client component reading +`useSearchParams()` from the App Router to sit inside a `` +boundary, because the hook's value depends on runtime query string +data that isn't available at prerender time. Without the boundary the +prerender step fails. + +**Fix.** Wrap the consumer: + +```tsx +import { Suspense } from 'react'; + +export default function Page() { + return ( + Loading…}> + + + ); +} + +function PageContents() { + const params = useSearchParams(); + // … +} +``` + +Move the `useSearchParams()` call into the inner component. The outer +page becomes a thin wrapper that owns the boundary. + +### 3.4 Admin prerender failures on routes that hit the API + +**Symptom.** `next build` fails with `fetch failed` errors during the +"Collecting page data" step. The failing routes are ones that call the +API server-side. + +**Cause.** Next.js statically generates routes at build time. If a +route's `generateStaticParams` or server-side data fetcher hits the +API, the build step needs the API to be reachable from the *builder* +container — but in a fresh `make up`, the api service may not be +healthy yet when the admin image is being built. + +**Fix.** Two paths: + +- Mark the route as fully dynamic with `export const dynamic = 'force-dynamic'` + so it skips prerender entirely. Best for admin pages that always + need fresh data anyway. +- Or run `make up postgres redis minio api` first, wait for the api + to be healthy, *then* `make up` (which rebuilds admin against the + running api). + +The admin is auth-gated and short-lived, so most routes are +acceptable as `force-dynamic`. The block editor pages already are. + +--- + +## 4. Background workers and jobs + +### 4.1 Worker keeps restarting + +**Symptom.** `make ps` shows the `worker` container looping through +`Restarting`. `make logs worker` shows it exiting at boot with a +config-validation error. + +**Cause.** The worker shares the same env-loader contract as the api. +If a required secret (`GONEXT_AUTH_PEPPER`, `GONEXT_AUTH_SESSION_SECRET`, +`GONEXT_AUTH_CSRF_SECRET`) is missing or too short, it refuses to +boot. + +**Fix.** Check `make logs worker` for the specific missing key. +Compose's `docker-compose.dev.yml` ships dev defaults inline — if +you've overridden the env via `.env` or `docker-compose.override.yml`, +make sure all three secrets are at least 32 bytes. + +### 4.2 Image processing jobs sit forever in `pending` + +**Symptom.** You upload an image; the upload succeeds; the thumbnail +never appears. Job inspector (admin → Jobs) shows the task `pending`. + +**Cause.** The worker isn't running, or it can't reach MinIO. Image +processing pulls the original from S3, runs libvips, and writes back +— if any leg fails, the job retries with backoff. + +**Fix.** Check `make ps` for the worker container; check `make logs +worker` for the S3 / libvips error. If MinIO is healthy but the +worker can't reach it, the most likely culprit is an +`AWS_ENDPOINT_URL` mismatch — should be `http://minio:9000` inside +the Compose network, not `http://localhost:9000`. + +--- + +## 5. Resetting to a clean state + +When the easiest path is to wipe and start over: + +```bash +make down +docker compose -f docker-compose.yml -f docker-compose.dev.yml down -v +docker system prune -f # optional; reclaims build cache +make up +docker compose run --rm migrate gonext init \ + --admin-email you@example.com \ + --admin-password 'replace-me' \ + --site-name 'My Site' \ + --site-url http://localhost:3000 +``` + +The `down -v` is the destructive bit — Postgres, Redis, and MinIO +volumes are gone. The build cache survives unless you also prune. + +--- + +## 6. Getting more diagnostics + +| Need | How | +| --- | --- | +| All service logs (live) | `make logs` | +| One service's logs | `docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f api` | +| API config (secrets masked) | `docker compose run --rm migrate gonext config dump` | +| psql shell | `make psql` | +| Redis shell | `make redis-cli` | +| Full smoke probe | `make smoke` (brings up, probes every healthz, tears down) | +| Crank up API logs | `GONEXT_LOG_LEVEL=DEBUG` and `GONEXT_LOG_ADDSRC=true` in `docker-compose.override.yml` | + +If you've ruled the symptom out as a known issue and the stack is +still misbehaving, open a GitHub issue with the output of `make ps`, +the failing logs, and the contents of any `docker-compose.override.yml` +you have in the tree.