From ef173f022d8e6016d512e729982bf34726180e95 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Wed, 1 Jul 2026 03:21:41 +0300 Subject: [PATCH] docs: add "Running a local dev stand" guide + reference it from AGENTS.md Captures the non-obvious gotchas that make bringing up a local instance painful: the collaboration server is a THIRD process (pnpm dev starts only API + client) that must be built before running (tsx/ts-node fail on NestJS DI); APP_SECRET must be identical between the API and collab servers or every realtime connection is rejected with "Invalid collab token"; Vite binds localhost so LAN access needs --host; a stale @docmost/editor-ext white- screens the client; pgvector is mandatory; migrations don't auto-run in dev. Also documents that demo/test passwords should be a simple one-word alphanumeric (no special chars, which get mangled through shells/JSON/URLs). Referenced from AGENTS.md (Commands + Two-server-processes sections). Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 8 +++ docs/dev-stand.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 docs/dev-stand.md diff --git a/AGENTS.md b/AGENTS.md index 70a382f7..b2e1efcc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -197,6 +197,12 @@ pnpm workspace (`pnpm@10.4.0`) orchestrated by **Nx**. Four workspace packages: Run from the repo root unless noted. The dev workflow needs **Postgres (with the `pgvector` extension) and Redis** reachable per `.env` (copy `.env.example` → `.env`). +> **Bringing up a full local stand** (API + client + the separate realtime +> collaboration process) has several non-obvious gotchas — a missing collab +> server, `APP_SECRET` mismatch between processes, a stale `editor-ext` white- +> screening the client, LAN exposure. See **[docs/dev-stand.md](docs/dev-stand.md)** +> for the step-by-step and the traps. + ```bash pnpm install # install all workspaces (uses pnpm patches; see package.json `pnpm.patchedDependencies`) pnpm dev # client (Vite) + server (Nest watch) concurrently — primary dev loop @@ -241,6 +247,8 @@ Migration files live in `apps/server/src/database/migrations/` and are named `YY - **API server** — `dist/main` (`apps/server/src/main.ts`), the Fastify HTTP app (`AppModule`). - **Collaboration server** — `dist/collaboration/server/collab-main` (`pnpm collab`), a Hocuspocus/Yjs WebSocket server (`apps/server/src/collaboration/`) handling real-time document editing, persistence, and page-history snapshots. It listens on `COLLAB_PORT` (default `3001`), separate from the API server's `PORT` (default `3000`), and shares state with the API server through Redis. +`pnpm dev` starts **only** the API server + client — the collaboration process is separate and must be started too, or the editor never connects. See **[docs/dev-stand.md](docs/dev-stand.md)** for running both locally (and why `APP_SECRET` must match between them). + The API server is a Fastify app with a global `/api` prefix (`main.ts` excludes `robots.txt`, public share pages, and `mcp` from the prefix). A `preHandler` hook enforces that a resolved `workspaceId` exists for most `/api` routes (multi-tenant by hostname/subdomain via `DomainMiddleware`). `GET /api/sb/:id` (the anonymous blob-sandbox read route) is listed in that preHandler's `excludedPaths`, so it is exempt from workspace resolution and carries no session auth at all (its capability is the unguessable UUID + TTL + TLS) — unlike `/api/files/public/...`, which still resolves a workspace and requires a workspace-bound attachment JWT. Auth is JWT (cookie + bearer); authorization is **CASL** (`core/casl`) — every data access is scoped to the user's abilities. ### Module structure (server) diff --git a/docs/dev-stand.md b/docs/dev-stand.md new file mode 100644 index 00000000..2fc47939 --- /dev/null +++ b/docs/dev-stand.md @@ -0,0 +1,135 @@ +# Running a local dev stand + +How to bring up a working local instance (API + client + realtime collaboration) +and the non-obvious gotchas that will otherwise eat an hour. Written from real +setup pain — read the **Gotchas** section before you start. + +## Prerequisites + +- **Node 20+ / pnpm 10+.** +- **Postgres with pgvector.** Use the `pgvector/pgvector` image (e.g. + `pgvector/pgvector:pg18`). The stock `postgres` image will FAIL the + `CREATE EXTENSION vector` migration — the RAG feature stores embeddings in + `page_embeddings`. +- **Redis** — backs caching, BullMQ queues, the Socket.IO adapter, and collab + sync. + +## 1. Environment (`.env`) + +The client (`apps/client/vite.config.ts`) and both server processes read env via +`envPath` → the **workspace root `.env`**. Keep a single source of truth. Minimum: + +```dotenv +APP_URL=http://localhost:3000 +PORT=3000 +APP_SECRET= +DATABASE_URL="postgresql://:@localhost:5432/?schema=public" +REDIS_URL=redis://127.0.0.1:6379 +COLLAB_URL=http://localhost:3001 # where the CLIENT connects for realtime +COLLAB_PORT=3001 # where the COLLAB server listens +STORAGE_DRIVER=local +DISABLE_TELEMETRY=true +``` + +> If you also keep an `apps/server/.env`, its `APP_SECRET` **must match** the +> root one (see gotcha #3). + +## 2. Migrations + +Migrations do **not** auto-run in local dev. After a fresh checkout or switching +branches, apply them yourself or endpoints touching a new column/table will 500: + +```bash +pnpm --filter server migration:latest +``` + +## 3. Bring it up — THREE processes, not two + +`pnpm dev` starts only the **API server** (Nest, `:3000`) and the **client** +(Vite). Realtime collaboration is a **separate process** and `pnpm dev` does NOT +start it. You need all three: + +```bash +# 1) API + client (from the repo root) +pnpm dev +# → API http://localhost:3000 +# → client http://localhost:5173 (Vite; localhost-only by default) + +# 2) Collaboration server — SEPARATE process. Build first (see gotcha #2), then: +pnpm --filter server build # produces dist/collaboration/server/collab-main.js +pnpm collab:dev # node dist/.../collab-main → listens on :3001 (0.0.0.0) +``` + +Without step 2 the editor shows **"Real-time editor connection lost. Retrying…"**, +stays in read-only *static* mode, and anything that only mounts in the *live* +editor won't appear. + +## Seeding a login + +Register through the UI, or reset an existing user's password directly in the DB +(the server hashes with `bcrypt`): + +```js +// node -e '...' with pg + bcrypt from the repo's node_modules +const bcrypt = require("bcrypt"); +const { Client } = require("pg"); +(async () => { + const hash = await bcrypt.hash("demopass", 10); + const c = new Client({ /* DATABASE_URL parts */ }); + await c.connect(); + await c.query("update users set password=$1 where email=$2", [hash, "admin@example.com"]); + await c.end(); +})(); +``` + +> **Use a simple one-word password with no special characters** (e.g. `demopass`, +> not `Str0ng!Pass@2026`). Demo/test credentials get passed through shells, JSON +> payloads, and URLs by scripts and automation, where `!` `@` `$` `&` etc. get +> mangled or need escaping — a plain alphanumeric word avoids a whole class of +> "wrong password" confusion. + +## Gotchas (the грабли) + +1. **Collaboration is a third process.** `pnpm dev` runs API + client only. + Start `pnpm collab:dev` (on `:3001`) separately or the live editor never + connects. The client connects to `COLLAB_URL` directly (default + `http://localhost:3001`), NOT through the Vite `/collab` proxy — the API + server on `:3000` does **not** serve the collab websocket. + +2. **The collab server must be built — you can't run it from source.** + `collab:dev` runs `node dist/collaboration/server/collab-main.js`, so run + `pnpm --filter server build` first. Running the entry via `tsx`/`ts-node` + fails with a NestJS DI error ("dependency … appears to be undefined at + runtime") because direct TS execution doesn't emit the decorator metadata the + built output has. + +3. **`APP_SECRET` must be identical for the API server and the collab server.** + The API issues a collab-token (JWT signed with `APP_SECRET`); the collab + server validates it with `APP_SECRET`. If they load different values (e.g. a + root `.env` and an `apps/server/.env` with different secrets), every realtime + connection is rejected with **`[onAuthenticate] Invalid collab token`** and + the editor shows "connection lost". Keep one secret everywhere. + +4. **Vite binds localhost only.** To reach the stand from another machine on the + LAN, start the client with `--host` (`pnpm --filter client exec vite --host`) + and use the box's LAN IP. The `/api`, `/socket.io`, and `/collab` Vite proxies + forward to `APP_URL`, so the API just works over the LAN; realtime needs + `COLLAB_URL` reachable from the browser (point it at the LAN IP:3001, and run + collab on `0.0.0.0` — it does by default). + +5. **A stale `@docmost/editor-ext` white-screens the client.** The client imports + from `@docmost/editor-ext` (a workspace package). If that package's source is + behind (missing a newer export, e.g. `Spoiler`), the client dies at load with + *"The requested module … does not provide an export named 'Spoiler'"* → blank + page. Make sure the workspace `packages/editor-ext` is current for the branch + you're running (a stale sibling checkout resolved through a shared + `node_modules` symlink is the usual cause). + +6. **pgvector, not stock postgres** (see Prerequisites) — the `vector` extension + migration fails otherwise. + +7. **Migrations don't auto-run in dev** — run `migration:latest` after every pull + or branch switch. + +See also the **Commands** and **Architecture → Two server processes** sections in +[`AGENTS.md`](../AGENTS.md).