Ctrl K
ComunidadGuías…

CLAUDE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Cloud Codex — Agent Guide

> Self-hosted, real-time collaborative documentation platform.
> Confluence/Notion alternative for teams that need to own their data.
> © Cloud City Computing, LLC.

## What this product is

**Cloud Codex** is a multi-user web app for creating, editing, and organizing
team documentation. Differentiating capabilities:

- **Real-time collaborative editing** via Yjs CRDT — multiple users edit the
  same document simultaneously with conflict-free merging, not last-write-wins.
- **Hierarchical organization**: Workspaces → Squads → Archives → Logs (documents).
- **Layered access control** (admin → user grants → creator → workspace owner →
  squad role → squad grants → workspace-wide flag).
- **Invite-only registration** (no public signup; admin issues invite tokens).
- **GitHub integration** as a live API proxy: browse repos, edit files, manage
  branches/PRs, link documents to GitHub files. No webhooks, no background sync.
- **Self-hosted**: Docker Compose for dev and prod. MySQL 8 + Node 22.

The "c2" repo name is a legacy codename — refer to the product as **Cloud Codex**
in user-facing strings, comments, and commit messages.

## Repository layout (the dual-root quirk)

The repo root holds Docker, docs, and SQL. The Node application lives one level
down in `cloudcodex/`. **Run all `npm` commands from `cloudcodex/`.**

```
c2/                              ← repo root (Docker, docs, SQL, Make)
├── docker-compose.yaml          ← dev: MySQL only
├── docker-compose-prod.yml      ← prod: MySQL + app
├── docker-compose.linux.yml     ← native-Linux :Z override for the dev file (not WSL)
├── Makefile                     ← seed, reset-db, db-shell, backup, restore
├── start.sh                     ← one-shot dev bootstrap
├── scripts/                     ← backup.sh, restore.sh (Bash; docs/deployment.md, Backups)
├── init.sql / seed.sql          ← schema + sample data
├── migrations/                  ← incremental SQL migrations
├── docs/                        ← human-facing architecture docs
│   └── maps/                    ← file:line-cited deep maps, READ THESE FIRST
├── .github/workflows/ci.yml     ← audit, lint, test, coverage, build; push to main + every PR
└── cloudcodex/                  ← the Node application
    ├── app.js                   ← Express app (middleware, route mounting)
    ├── server.js                ← entry point (verifies SMTP + admin, WS attach)
    ├── mysql_connect.js         ← DB pool, sessions, c2_query()
    ├── vite.config.js           ← code-splitting strategy (read before adding deps)
    ├── vitest.config.js         ← four projects + 48 per-glob coverage thresholds
    ├── eslint.config.js         ← strict flat config
    ├── routes/                  ← API endpoints
    │   ├── helpers/             ← shared.js, ownership.js, images.js,
    │   │                          activity.js, mentions.js, REUSE THESE
    │   ├── auth.js, documents.js, archives.js, workspaces.js, squads.js,
    │   ├── comments.js, search.js, favorites.js, admin.js,
    │   ├── oauth.js, github.js, avatars.js, doc-images.js,
    │   ├── doc-images-serve.js (the /doc-images mount), upload.js,
    │   ├── notifications.js, activity.js, watches.js
    ├── middleware/              ← auth.js (requireAuth, requireAdmin), permissions.js,
    │                              forwarded-for.js (the untrusted-proxy warning)
    ├── services/                ← collab.js (Yjs WS), user-channel.js (inbox WS),
    │                              notifications.js, email.js, email-templates.js
    ├── src/                     ← React frontend
    │   ├── App.jsx              ← router + lazy-loaded pages
    │   ├── pages/               ← full-page components
    │   ├── components/          ← reusable UI (+ components/github/ pickers)
    │   ├── extensions/          ← custom Tiptap nodes (Mention, GitHub embeds)
    │   ├── hooks/               ← useCollab, usePresence, useClickOutside,
    │   │                          useGitHubStatus.jsx, useGitHubLink,
    │   │                          useNotificationChannel
    │   ├── lib/githubDiff.js    ← diff3 merge, imported by the BACKEND too
    │   ├── util.jsx             ← apiFetch + API wrappers — USE THIS for new calls
    │   ├── userPrefs.js         ← localStorage prefs + theme constants
    │   ├── editorUtils.js       ← editor-specific helpers
    │   └── codex.css            ← --cx- theme bindings over the vendored primitives
    ├── vendor/cloud-city-design/ ← vendored design package, pinned by checksum; NEVER edit here
    ├── public/                  ← uploaded avatars, document images
    └── tests/                   ← Vitest + Supertest, mirrors the source tree
```

## Deep maps: read before changing anything cross-cutting

`docs/maps/` holds `file:line`-cited maps of every subsystem. They exist so you
do not re-derive the wiring by re-reading the tree, and they carry the traps this
file only summarises. Start at [docs/maps/README.md](docs/maps/README.md).

| Map | Read it when |
|---|---|
| `request-lifecycle.md` | boot order, middleware stack, sessions, WS upgrades, error handling, health probes, the instance lock, shutdown |
| `access-control.md` | **any permission work**, the 7-clause fragments and the four other permission systems |
| `documents-and-collab.md` | the editor, Yjs, dual-state storage, versions |
| `github-integration.md` | anything under `routes/github.js` |
| `notifications-and-activity.md` | notifications, activity log, watches, mentions |
| `data-model.md` | schema changes, migrations, the ACL columns |
| `frontend-architecture.md` | routing, chunks, hooks, `util.jsx`, prefs |
| `build-test-and-ops.md` | Docker, npm scripts, Vitest projects, coverage thresholds, CI |
| `open-questions.md` | **before trusting any single claim**, known dead paths and suspected defects |

`docs/*.md` remains the human-facing product documentation (features,
deployment, API contracts). `docs/maps/` is the mechanism layer.

## Application areas (what already exists)

Before writing new code in any of these areas, **read the existing files**.
Most "new feature" work is extension, not greenfield.

### Auth & accounts: `routes/auth.js`, `routes/oauth.js`, `routes/oidc.js`, `middleware/auth.js`
Email+password login, signup via invite token, password reset, two-factor (email
OTP and TOTP with QR), session tokens (DB-backed, one row per sign-in, stored
only as a SHA-256 digest), Google Workspace SSO, OIDC sign-in with any standard
issuer, one per instance (`services/oidc.js`, `OIDC_*`, keyed on `(issuer, subject)` in
`user_identities`, `returnTo` by `services/return-to.js`; setup and provider notes in
`docs/sign-in.md`; `AUTH_PROVIDERS=oidc` is OIDC-only mode), GitHub OAuth (token
AES-256-GCM encrypted at rest). Use `requireAuth` and `requireAdmin` from
`middleware/auth.js` on any new protected route. **Never bind a raw session
token into a `sessions` query**: hash it with `hashSessionToken` in
`services/session-token.js` (`validateAndAutoLogin` and `touchSession` already
do). The cookie is `__Host-sessionToken` on https (`sessionToken` over plain
http); read it only through `extractSessionToken` on the server and
`getSessionTokenFromCookie` on the client, write it only through
`setSessionCookie`, and never give it a `Domain`
(`services/session-cookie.js`). Match cookie names exactly: strip only ASCII
space and tab (`COOKIE_OWS`), never `trim()`. An `/api` write authenticated by that cookie
alone needs an accepted `Origin` (`requireOriginForCookieWrites` in `app.js`).

**External sign-in** decides its local user in one seam, `resolveIdentity(claims,
policy)` in `services/identity.js`: a provider route verifies the protocol and
hands over claims plus a policy, and gets back a user id or a named refusal.
Google and OIDC are its callers (the `oidc` branch is `resolveOidcIdentity`). Do not
write a second account ladder in a route. OIDC never creates an account. `parseAuthProviders()` in the same file validates `AUTH_PROVIDERS` at
boot (unset means today's set).

**Sign-out** (W6-CDX-6): `POST /api/logout` returns an `endSessionUrl` for an OIDC session, carrying the
ID token sealed by `services/id-token-seal.js` as `id_token_hint`. `POST /api/auth/oidc/backchannel-logout`
(`routes/oidc.js`, OIDC only) ends sessions by sid or subject, bounded by the token's `iat`, and closes their
sockets with 4004 (`closeSocketsForSessions` in `services/collab.js` and `services/user-channel.js`). Maps:
`docs/maps/request-lifecycle.md`, section 3.

**No machine credential**: every route is reached by a session (the bearer header or the cookie). A script signs in as a user.

### Org hierarchy: `routes/workspaces.js`, `routes/squads.js`, `routes/archives.js`
Workspaces own squads; squads own archives; archives own logs. Squad members
have roles (member/admin/owner) and per-member permission flags
(read/write/create_log/create_archive/manage_members/delete_version/publish).
Squad invitations have a pending/accepted/declined lifecycle. **All access
checks go through `routes/helpers/ownership.js` — do not re-derive permission
SQL inline.** The **archive is the ACL boundary**; per-document permissions do
not exist. `docs/maps/access-control.md` has the full resolution table.

### Documents & editor: `routes/documents.js`, `src/pages/Editor.jsx`, `services/collab.js`
Logs (documents) have **dual-state storage**: `html_content` (human-readable,
written by REST saves and exports) and `ydoc_state` BLOB (binary CRDT state for
live sync restore), plus `markdown_content` for lossless GitHub round-tripping.
The Yjs WebSocket server in `services/collab.js` debounces saves on a 3-second
window, and that autosave writes **only** the blob. `html_content` updates on
an explicit save or publish, so search, export and GitHub push all read the
last explicitly-saved HTML. Any code path that writes `html_content` from
outside the editor **must set `ydoc_state = NULL`** or live editors will
resurrect the old content. Editor uses Tiptap 3 + ProseMirror with Lowlight
syntax highlighting, draw.io diagrams, resizable images, mentions, and GitHub
code/issue embeds. Full protocol in `docs/maps/documents-and-collab.md`.

### Notifications: `services/notifications.js`, `routes/notifications.js`, `services/user-channel.js`
One funnel for every user-facing alert. `createNotification()` self-suppresses
actor-equals-recipient, coalesces the same (recipient, type, resource) inside a
60-second window, persists, then fire-and-forgets a WebSocket push and an email.
Six types (`mention`, `comment_on_my_doc`, `watched_comment`, `watched_publish`,
`watched_log_update`, `squad_invite`) with per-user email preferences in
`users.notification_prefs`. The user-scoped WS (`/notifications-ws`) is
push-only and keyed by user, not by document.

### Activity & watches: `routes/helpers/activity.js`, `routes/activity.js`, `routes/watches.js`
`logActivity()` is fire-and-forget and **does four things**: writes the
`activity_log` row, emits an outbound webhook event (below), auto-enrols the
actor as a watcher, and fans notifications out to every other watcher. Adding
a `logActivity` call to a route therefore starts sending people email. `log.update` events coalesce on a 5-minute window
(distinct from the notification funnel's 60 seconds). Watching an archive
cascades to its documents. Retention is 365 days, pruned in-process daily.

### Outbound webhooks: `services/webhooks.js`, `services/webhook-target.js`, `routes/webhooks.js`
`emitEvent`, called once inside `logActivity` after the activity row, writes
one outbox event (`webhook_events`, the exact body bytes) and its deliveries
for eight document and archive actions, **only when a cached subscription
matches**, so an install with none adds no query to any change. It never
throws. Every receiver URL goes through `checkWebhookTarget` (the SSRF
guard); do not build a second check. A subscription's secret is never
stored; it is returned only once, on creation or rotation. Contract:
`docs/api/webhooks.md`; mechanism: `docs/maps/notifications-and-activity.md`
section 2a. The delivery worker (`services/webhook-worker.js`, W6-CDX-14) sends them from
an in-process tick every two seconds: one head per subscription, leased, in order; the
mechanism is in `docs/maps/notifications-and-activity.md` section 2b.

### Mentions: `routes/helpers/mentions.js`, `src/extensions/Mention.jsx`
`<span data-mention-user-id="N">` nodes, diffed old-vs-new HTML so only newly
added mentions notify. Mentions inside `<pre>`, `<code>`, `<script>`, `<style>`
are ignored, and every recipient is re-checked for read access before being
notified.

### Comments: `routes/comments.js`, `src/components/Comment*.jsx`
Text-anchored inline comments (selection_start/end), tags
(comment/suggestion/question/issue/note), open→resolved/dismissed status,
threaded replies, real-time WS broadcast via the collab service.

### Versions & publishing: `routes/documents.js` (`/document/:logId/publish`, `/versions/*`)
Named snapshots with release notes. Publish is permission-gated. Restore writes
back to current state. Version delete is permission-gated.

### Import / Export: `routes/upload.js`, `routes/documents.js` (`/export`)
Import: HTML, Markdown, plain text, PDF (`pdf-parse`), DOCX (`mammoth`).
Export: DOCX (`html-to-docx`), HTML, Markdown (`turndown`), plain text, PDF
(browser print). Image extraction/inlining handled by `routes/helpers/images.js`.

### Search & browse: `routes/search.js`
MySQL FULLTEXT index on `logs(title, plain_content)`. No external search
service. `/browse` returns recent + favorited documents for users with no
query. **Do not propose Elasticsearch/Meilisearch/etc. without a strong reason.**

### Favorites: `routes/favorites.js`
Per-user mark/unmark/list/check.

### GitHub integration: `routes/github.js`, `src/pages/GitHubPage.jsx`, `src/hooks/useGitHubStatus.jsx`
Live API proxy, ~45 endpoints, no webhooks and no background sync. Repo list,
branches, file tree, read/write/delete/rename contents, PRs, commits, file
search. Plus four later phases:
- **Bidirectional sync**: `github_links` tracks a merge base (`base_sha`) and a
  5-state `sync_status`. Pull does a local diff3 merge (`src/lib/githubDiff.js`)
  and 409s on conflict; push commits directly or opens a PR.
- **Live code embeds**: `/api/github/embed/code`, cached in a 500-entry LRU
  **keyed by user id** so private-repo content cannot leak across users.
- **PR-as-document**: a PR gets a virtual log in a hidden `system` archive so the
  normal comment routes work on it (see `docs/maps/open-questions.md` first).
- **Squad↔GitHub Team sync**: manual, one-directional, matched on
  `oauth_accounts.provider_username`.

`routes/github.js` **does not use the shared `errorHandler`**, it ships its own
so upstream GitHub status codes and messages reach the client. Don't "fix" that.
UI hides GitHub affordances when the user hasn't linked, use `useGitHubStatus`.

### Admin console: `routes/admin.js`, `src/pages/AdminPage.jsx`
Platform stats, workspace/user/invitation/squad management, live presence
telemetry, permission flag toggles, and the outbound webhook panel
(`src/components/WebhooksPanel.jsx`, over `routes/webhooks.js`). Admin user is auto-synced from `.env` on
startup (see `server.js`): created, or an existing admin's email and password
reset, and **never promoted** from an existing non-admin account, which boot
refuses and logs (`docs/maps/access-control.md` section 6).

### User preferences: `src/userPrefs.js`
Editor mode (WYSIWYG vs Markdown), accent color, editor font size. Density and the sidebar default were removed (W6-CDX-42).
Stored in localStorage. New preferences go through `loadUserPrefs` /
`saveUserPrefs` and `applyPrefsToDOM` — don't write to localStorage directly.

## Critical architectural decisions (don't undo without discussing)

1. **Single Node process** runs Express, Vite (in dev), and **both** WebSocket
   servers (`/collab` and `/notifications-ws`). No external broker, no separate
   collab service. This is load-bearing: collab state is an in-memory `Y.Doc`
   per open document, so a second replica would maintain a second, divergent
   copy of the same log. The instance lock (`services/instance-lock.js`, a
   `GET_LOCK` named for the schema) makes a second process refuse to start;
   `C2_INSTANCE_LOCK=0` is the escape, not a scaling switch.
2. **Dual-state document storage** (`html_content` + `ydoc_state`, plus
   `markdown_content`). REST saves write `html_content`; live editing writes
   `ydoc_state`. They converge only on an explicit save or publish.
3. **MySQL FULLTEXT only** for search, over the generated `logs.plain_content`
   column. No external index.
4. **Invite-only signup**. The `users` table is never written without a valid
   invite token, an admin action, or an OAuth flow with a valid provider config.
   OIDC sign-in links an existing account, or refuses; it never creates one.
5. **OAuth tokens encrypted at rest** with AES-256-GCM, key derived from
   `GITHUB_CLIENT_SECRET` via scrypt. Never log raw tokens. Rotating that secret
   invalidates every stored token, there is no key version and no re-encryption
   path.
6. **Layered access control** lives in `routes/helpers/ownership.js`. All
   read/write checks compose `readAccessWhere()`/`writeAccessWhere()` SQL
   fragments and pass `readAccessParams(user)`/`writeAccessParams(user)`. This
   resolves admin → user JSON → creator → workspace owner → squad role → squad
   JSON → workspace-wide flag in one query. Don't shortcut it.
   **The fragment and the params are coupled by position: always exactly 7
   params.** Adding a clause without adding its param shifts every later `?` in
   the whole query and silently returns wrong rows rather than erroring. Change
   one, change all four, and update `tests/helpers/ownership.test.js`.
7. **Per-router error handlers.** Each router ends with
   `router.use(errorHandler)`; `app.js` mounts no global handler, so a router
   that forgets it leaks Express's default HTML stack trace. `routes/github.js`
   deliberately uses its own instead.

## Gotchas that have already cost time

Each is expanded, with citations, in the map named after it.

- **The archive is the ACL boundary.** `logs.read_access` / `logs.write_access`
  exist in the schema, are written by one code path, and are read by **nothing**.
  Granting access by writing them will appear to work and grant nothing.
- **CRDT autosave does not write `html_content`.** A document edited only over
  the WebSocket is stale in search, in every export format, and on GitHub push
  until an explicit save.
- **External writers of `html_content` must null `ydoc_state`.** The GitHub pull
  paths and version restore do this; copy that pattern.
- **`init.sql` only runs on a fresh MySQL volume.** Docker skips
  `docker-entrypoint-initdb.d` on an initialised data dir. Schema changes need a
  `migrations/` file **and** an `init.sql` edit. Apply the migration with
  `npm run migrate`; `schema_migrations` records what ran. Each database needs
  one adoption command first: `--baseline` if it existed before the runner (the
  usual case), `--adopt-fresh-install` only if `init.sql` just built it, and
  that one now verifies each post-baseline file against the live schema and
  refuses when the change is missing. The `LEGACY_BASELINE` list in
  `scripts/migrate.js` is closed: never append a new migration to it, or
  `--baseline` will mark it applied without running it. In containers the runner
  goes through `docker compose ... run --rm app`, never `exec`.
- **`make reset-db` is not a clean reset.** `init.sql`'s DROP list omits four
  tables (`github_links`, `activity_log`, `watches`, `notifications`) whose
  CREATEs lack `IF NOT EXISTS`, so it aborts partway on an existing database.
- **Coverage thresholds are per-glob and CI enforces them.** Adding an uncovered
  branch to a high-threshold file fails the build even when every test passes.
- **Backend tests queue `c2_query` mocks in call order.** Adding a query to a
  handler shifts the queue and breaks tests that were passing correctly. Check
  the mock ordering before assuming your change is wrong.
- **`src/lib/githubDiff.js` is imported by the backend.** It lives under `src/`
  but changing its exports breaks the server.
- **Widening a content column means widening three.** `logs.html_content`,
  the `plain_content` generated from it, and `versions.html_content` (publish
  copies the document into it) all have to move together, or the same error
  just relocates. All three are `MEDIUMTEXT` as of 2026-08-09; the app's 2 MiB
  ceiling is now the real one. See `docs/maps/open-questions.md` B2.

## Development commands

All `npm` commands run from `cloudcodex/` unless noted.

```
npm install                  # install deps
npm run dev                  # Vite HMR + Express, single process
npm run build                # production frontend build
npm run preview              # preview the prod build
npm run lint                 # ESLint over the whole package
npm test                     # Vitest, single run
npm run test:watch           # Vitest watch
npm run test:coverage        # coverage report
npm run test:integration     # opt-in live-MySQL project (needs IT_DB_ROOT_PASSWORD)
npm run migrate              # apply pending migrations/*.sql
npm run migrate -- --adopt-fresh-install  # once, on a database init.sql built
npm run migrate -- --baseline             # once, on a pre-runner install
```

From the repo root:

```
make seed                    # load seed.sql
make reset-db                # rerun init.sql + seed.sql
make db-shell                # mysql CLI in the Docker container
./start.sh                   # one-shot bootstrap (deps, Docker, dev)
```

CI (`.github/workflows/ci.yml`) is one job, `Lint, test and build`, which runs
`npm ci`, `npm audit --omit=dev --audit-level=moderate` (blocking: a moderate or
worse advisory in a production dependency fails the build, even on a PR that
touched no dependency; a plain `npm audit` follows, advisory only),
`npm run lint`, ShellCheck over `scripts/*.sh`, `npm test`,
`npm run test:integration` (against a
`mysql:8.4.11` service container in the same job), `npm run test:coverage` (the
per-glob thresholds are the real gate) and `npm run build`. It runs on push to `main`
and on every pull request whatever its base, and it is the required status
check on `main`. **There are no pre-commit hooks**, so local lint and test are
on you.

## Rules

### Reuse before adding

The single most common failure mode for an AI agent in this repo is to write a
new helper that already exists. **Before adding any utility, search for an
existing one.**

| Need                                       | Use                                                       |
|--------------------------------------------|-----------------------------------------------------------|
| Validate a numeric ID                      | `isValidId` in `routes/helpers/shared.js`                 |
| Wrap an async route handler                | `asyncHandler` in `routes/helpers/shared.js`              |
| Sanitize user-supplied HTML                | `sanitizeHtml` in `routes/helpers/shared.js`              |
| Read or write access SQL fragments         | `routes/helpers/ownership.js`                             |
| Check log read/write access in code        | `checkLogReadAccess` / `checkLogWriteAccess` in `shared.js` |
| Check archive read/write access in code    | `checkArchiveReadAccess` / `checkArchiveWriteAccess` in `shared.js` |
| Check ownership for a destructive action   | `isArchiveOwner` in `routes/helpers/ownership.js`         |
| Check publish permission                   | `canPublish` in `shared.js`                               |
| Require authentication on a route          | `requireAuth` in `middleware/auth.js`                     |
| Map a verified external identity to a user | `resolveIdentity` in `services/identity.js`               |
| Require admin on a route                   | `requireAdmin` in `middleware/auth.js`                    |
| Require a permission flag                  | `requirePermission(flag)` in `middleware/permissions.js`  |
| Run a SQL query                            | `c2_query(sql, params)` in `mysql_connect.js`             |
| Record an audit/activity event             | `logActivity` in `routes/helpers/activity.js`             |
| Alert a user (inbox + push + email)        | `createNotification` in `services/notifications.js`       |
| Check a webhook receiver URL (SSRF guard)  | `checkWebhookTarget` in `services/webhook-target.js`      |
| Notify on new @mentions in saved content   | `processMentionsOnSave` in `routes/helpers/mentions.js`   |
| Push a message to a user's open tabs       | `broadcastToUser` in `services/user-channel.js`           |
| Push a message to a document's live editors| `broadcastToDoc` in `services/collab.js`                  |
| Make an authenticated frontend API call    | `apiFetch(method, url, data)` in `src/util.jsx`           |
| Read/write user preferences                | `loadUserPrefs` / `saveUserPrefs` in `src/userPrefs.js`   |
| Subscribe to real-time doc updates         | `useCollab` in `src/hooks/useCollab.js`                   |
| Gate UI on GitHub being linked             | `useGitHubStatus` in `src/hooks/useGitHubStatus.jsx`      |
| Show presence avatars                      | `usePresence` + `<PresenceAvatars>`                       |
| Toast / confirm dialog                     | `<Toast>`, `<ConfirmDialog>` in `src/components/`         |
| Send an email                              | `sendEmail` in `services/email.js`                        |
| Build a notification email body            | `buildNotificationEmail` in `services/email-templates.js` |
| Extract / inline images for export         | `routes/helpers/images.js`                                |
| Record the images a write adds to a document | `recordDocImages` (with `introducedDocImages`) in `routes/helpers/images.js` |
| Check who may see a document image         | `readableDocImageHashes` in `routes/helpers/images.js`    |
| Three-way merge two markdown revisions     | `diff3Merge` in `src/lib/githubDiff.js`                   |

If you genuinely need a new helper: place it next to its peers (route helpers
in `routes/helpers/`, frontend utils in `src/`), match the export style of the
file it joins, and add a JSDoc block if it's a public utility.

### Linting and style

ESLint (`eslint.config.js`) is the source of truth. Notable rules:

- `no-var`, `prefer-const`, `eqeqeq` — modern JS only.
- `no-console: warn (allow: [error])` — **never `console.log` in committed
  code**. Use `console.error` for errors with the project format:
  `` `[${new Date().toISOString()}] ${req.method} ${req.path}:` `` plus the error.
- `no-alert` — **don't use `window.alert`** (a couple of legacy disables exist
  in `pages/Editor.jsx` and `pages/GitHubPage.jsx` — don't add more).
- `no-implicit-coercion` — no `!!x` or `+x` shortcuts.
- `react/jsx-no-useless-fragment`, `react/self-closing-comp` — clean JSX.
- `react/jsx-handler-names` — handler props are `onFoo`, handlers are `handleFoo`.
- `react-hooks/exhaustive-deps: warn` — don't silence by adding deps that cause
  loops; reach for `useCallback`/`useRef` first. There are a few `eslint-disable`
  lines in `GitHubPage.jsx` for this — they're load-bearing, leave them.

Run `npm run lint` before declaring done. CI will reject otherwise.

There is no Prettier config and no TypeScript. Match the surrounding file's
style: 2-space indent, single quotes in JS, double quotes in JSX attributes,
trailing semicolons.

### File header

Every source file in `cloudcodex/` opens with:

```javascript
/**
 * <One-line description of what this file does>
 *
 * All Rights Reserved to Cloud City Computing, LLC 2026
 * https://cloudcitycomputing.com
 */
```

New files match this pattern. Update the year only if the file is genuinely new.

### Naming

- Files: `kebab-case.js` for backend, `PascalCase.jsx` for React components,
  `camelCase.js` for frontend utilities and hooks (e.g., `useCollab.js`).
- Functions and variables: `camelCase`.
- React components: `PascalCase`.
- Constants: `UPPER_SNAKE_CASE` (`BCRYPT_ROUNDS`, `PASSWORD_MIN_LENGTH`).
- API URLs: `/api/kebab-case`.

### Error handling

- Routes return JSON `{ success: false, message: "<human-readable>" }` on
  failure. Some legacy routes omit `success` — if you're touching one, normalize
  it; otherwise leave it.
- HTTP codes: 400 validation, 401 auth, 403 access denied, 404 not found, 409
  conflict (e.g., duplicate email), 413 payload too large, 500 server error.
- Wrap every async route handler in `asyncHandler(...)`.
- Mount `errorHandler` from `routes/helpers/shared.js` at the end of each
  router file.
- Don't introduce a logging library. `console.error` with the established
  format is the convention.
- Don't catch errors just to swallow them. If a handler can't recover, let
  `asyncHandler` forward to `errorHandler`.

### Security (non-negotiable)

- **All SQL is parameterized.** `c2_query('... WHERE id = ?', [id])`. Never
  template-interpolate user input into a query string.
- **All user-supplied HTML passes through `sanitizeHtml`** before it touches
  the DB.
- **All access checks use `ownership.js` helpers.** Never write your own
  permission SQL.
- **Never log secrets** (passwords, OAuth tokens, session tokens, 2FA codes,
  reset tokens, SMTP creds).
- **Email content with user input** sanitizes `\r\n` to prevent header
  injection (see `services/email.js`).
- **Bcrypt rounds = 12.** Don't lower for speed.
- **Rate limiters are deliberate.** `authLimiter` (20/15min) on auth routes,
  `searchLimiter` (60/15min) on user search. New auth-adjacent routes get one.
  They key on `req.ip`, so `trust proxy` is part of them: it names the
  proxies by address (loopback, and the gateway of the network the
  production compose files pin to 172.29.0.0/16; never 172.17.0.1, which
  every IPv6 client of `docker run -p` arrives as), never a range, and a hop
  count, `true` or an over-wide range is refused at boot
  (GHSA-9fmx-frrf-xxmq). Do not relax either, do not unpin the network, and
  keep every published port on loopback by default (`APP_BIND`, `DB_BIND`;
  `tests/compose-ports.test.js`).

### Testing

- Framework: **Vitest 4 + Supertest** for backend, **Vitest + jsdom +
  @testing-library/react** for frontend. The two suites run as separate
  Vitest **projects** (configured in `vitest.config.js`); a single
  `npm test` runs both, and a third default project, `design`
  (`tests/design/`), runs the vendored design gates against an exemption
  ledger that may only shrink (lower `tests/design/ledger.json` by exactly
  what a change fixes). A fourth, opt-in project, `integration`, runs
  `tests/integration/` against a live MySQL (`npm run test:integration`,
  `IT_DB_ROOT_PASSWORD` required; see `cloudcodex/tests/README.md`). It
  proves `init.sql` builds on MySQL 8.4, adoption agrees with it, app SQL
  runs against the result, and (`upgrade-path.test.js`) every post-baseline
  migration file's SQL runs from the pre-runner state and lands on exactly
  the schema `init.sql` builds. A new migration needs its undo in
  `tests/integration/pre-runner-state.js`. Rows are never present during
  that upgrade, so a migration's data handling is not exercised.
- Tests mirror the source tree:
  - `routes/foo.js` → `tests/routes/foo.test.js`
  - `services/foo.js` → `tests/services/foo.test.js`
  - `routes/helpers/foo.js` → `tests/helpers/foo.test.js`
  - `src/foo.js` / `src/components/Foo.jsx` / `src/hooks/useFoo.js` → `tests/src/...`
- Use the helpers in `tests/helpers.js` (`mockAuthenticated`,
  `mockUnauthenticated`, `resetMocks`, `TEST_USER`, `TEST_USER_2`). There is no
  `ADMIN_USER`; admin tests pass
  `mockAuthenticated({ ...TEST_USER, is_admin: true })`.
- `tests/setup.js` mocks the DB (`c2_query`), email, and `sharp` globally for
  **backend** tests, your tests inherit these. Its `fs/promises` mock covers
  **only `mkdir` and `unlink`**; anything touching `stat`, `readFile` or
  `writeFile` (i.e. `routes/helpers/images.js`) must mock those itself.
  `tests/setup.frontend.js` provides jest-dom matchers and resets DOM /
  localStorage / sessionStorage between **frontend** tests.
- Because `c2_query` is a mock, most backend tests queue
  `mockResolvedValueOnce` calls **in the exact order the handler issues
  queries**. Adding a query to a handler shifts that queue and breaks tests that
  were passing for the right reason.
- Coverage runs via `npm run test:coverage` (uses `@vitest/coverage-v8`)
  and is enforced by per-glob thresholds in `vitest.config.js`. CI fails
  on threshold violations.
- **Adding or modifying a backend route, service, helper, or middleware
  requires updating the matching test file.** New code needs new tests
  in the same shape as its neighbors.
- **New or modified frontend hooks (`src/hooks/`), pure-JS utilities
  under `src/`, and reusable components in `src/components/` require
  matching tests.** Pages (`src/pages/`) remain out of scope by default —
  the giants like `Editor.jsx` and `GitHubPage.jsx` need a refactor that
  extracts logic into testable hooks before they're worth unit-testing.
- See `cloudcodex/tests/README.md` for testing patterns and examples.

### Shippability checklist

Before claiming a change is done:

1. `npm run lint` — clean, no new warnings.
2. `npm test` — green.
3. New env vars added to `.env.example` with a comment, and an entry in
   `cloudcodex/env-contract.js` (`tests/env-contract.test.js` fails without one).
   A `default` entry also needs a test that reads its value through
   `contractDefault('<NAME>')` and is listed in that test's `DEFAULT_PROVEN_IN`.
4. New required dependencies added to `package.json` (and code-split in
   `vite.config.js` if they're heavy frontend libs — see `manualChunks`).
5. Any new SQL columns/tables added as a migration in `migrations/` AND as
   part of `init.sql`. Both must stay in sync.
6. Any new admin-visible feature documented in the relevant `docs/*.md`.
7. Any change to architecture, data flow, a lifecycle, or a public surface
   reflected in the matching `docs/maps/*.md` **in the same PR**. A map that no
   longer matches the code is a bug. Anchor on names, not `:line`.
8. UI changes verified manually in the browser at the route they affect, on
   both desktop and mobile widths (mobile CSS is a recent investment area).
9. No `console.log` debug statements left in. No commented-out code.
10. No `TODO`/`FIXME` left in committed code without an issue link.

### What NOT to do

- Don't skip access control "just for an internal endpoint" — there are no
  internal endpoints, only public HTTP and the two WebSockets.
- Don't bypass `sanitizeHtml` because "this input is trusted" — it isn't.
- Don't hand-roll permission SQL, and don't add a per-row ACL column expecting
  it to work. Only `archives` is wired into `ownership.js`.
- Don't add a structured logging library, an APM agent, or a metrics service
  without discussing first. Plain `console.error` is the deliberate choice.
- Don't add a service worker, an external job queue, a Redis dependency, or a
  WebSocket server outside the app process. The single-process architecture is
  load-bearing for self-hosting: collab state is an in-memory `Y.Doc`.
- Don't replace the error handler in `routes/github.js` with the shared one
  it would flatten every GitHub 404 and rate-limit into an opaque 500.
- Don't add Prettier or migrate to TypeScript without discussing first.
- Don't propose Elasticsearch/Meilisearch — MySQL FULLTEXT is the choice.
- Don't `git push --force` on `main`. Don't `--no-verify`.
- Don't ship code with `.only` or `.skip` left in tests.
- Don't write to `localStorage` directly from React — go through `userPrefs`.
- Don't store new secrets in code or in commit history. `.env` only.

## Where to look first when you don't know

Pattern: **map for the mechanism, `docs/` for the product, source for the
truth.** The maps carry `file:line` citations so you land on the right line.

- **What does this product do?** → `README.md`, `docs/architecture.md`, `docs/features.md`.
- **How does anything actually work?** → `docs/maps/README.md`, then the map for the area.
- **How is data shaped?** → `docs/maps/data-model.md`, `init.sql`, `docs/database.md`.
- **How does access control resolve?** → `docs/maps/access-control.md`, `routes/helpers/ownership.js`, `docs/access-control.md`.
- **Why does the editor work the way it does?** → `docs/maps/documents-and-collab.md`, `services/collab.js`, `src/pages/Editor.jsx`.
- **Why is this GitHub route shaped like that?** → `docs/maps/github-integration.md`.
- **Why did that notification fire (or not)?** → `docs/maps/notifications-and-activity.md`.
- **Why is CI red when my tests pass?** → `docs/maps/build-test-and-ops.md` (per-glob coverage thresholds).
- **Is this a bug or is it meant to be like that?** → `docs/maps/open-questions.md`.
- **What env vars exist?** → `cloudcodex/env-contract.js` (every variable the server reads, pinned by a test), then `.env.example`.
- **What are the API contracts?** → `docs/api/*.md` (per-area).
- **What's the security model?** → `docs/security.md`.