App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
+579
View File
@@ -0,0 +1,579 @@
# Backend work — linear tickets
Copy each block into Linear (or your tracker) as a separate issue, **in order**. Earlier tickets are prerequisites for later ones.
**Foundation already in the repo (no ticket needed unless you are onboarding a greenfield clone):** Prisma schema ([prisma/schema.prisma](prisma/schema.prisma)), migrations, `lib/server/*`, Route Handlers under `app/api/*`, [docker-compose.yml](docker-compose.yml), [Dockerfile](Dockerfile), [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example), [lib/create/api.ts](lib/create/api.ts), create-flow draft **PUT** via `useCreateFlowExit` / `PostLoginDraftTransfer` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`.
### Review sync (relevant feedback only)
A backend review was merged into **[docs/backend-roadmap.md](backend-roadmap.md)** after checking the repo. **Incorporated:** custom session lifecycle follow-ups (not a mandate to adopt Auth.js/Lucia), **passwordless email (magic-link request)** rate limits in-memory until multi-instance + shared store, `RuleDraft` already has `updatedAt` (no migration to add it), **prefer external web vitals** over product Postgres by default, API error shape + request-id observability targets, **authorization v1** aligned with `app/api/rules`, Prisma **never edit applied migrations**, **profile / my rules / account** scope from Figma profile (`22143:900069`) as **Ticket 15** (change email deferred). **Excluded:** requiring NextAuth/Lucia; “add `updatedAt` on drafts”; hard ban on DB for vitals (softened to default external). **Parallel Linear issues:** **CR-84** (API errors — **unblocked** now that **CR-73** is Done), **CR-85** (session lifecycle — **unblocked** now that **CR-75** is Done)—see **Linear** table at the end of this doc.
---
## When you need server / admin access (and for what)
Use this if you **do not** have SSH or hosting access yet. Most engineering tickets are **local-only** until you deploy somewhere shared.
### You do **not** need the server admin for
- **Tickets 18, 10:** Everything runs on your machine: `docker compose up -d postgres mailhog`, `.env`, `npm run dev`, `npx prisma migrate dev`. **Magic-link** sign-in email can use Mailhog or **dev server logs** (verify URL) when `SMTP_URL` is unset—no real SMTP required locally.
- **Verifying APIs:** Use `localhost` and the same Docker Postgres—no production host.
### The **first** time you need someone with hosting access
That is when you deploy to **staging** or **production** (a URL other people use, or a persistent DB not on your laptop). Until then, you can finish the core product slice without server credentials.
Ask the admin to provide (or do for you) the items below—**Ticket 12** turns this into a written runbook.
| What | Why you need it |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Postgres** | Managed instance or container; a **`DATABASE_URL`** you can plug into the deployed app. |
| **Run migrations** | Someone runs **`npx prisma migrate deploy`** against that database **before** the new app version serves traffic (or gives you a secure way to run it in CI/CD). |
| **`SESSION_SECRET`** | Long random string in production env (sessions **+ hashed magic-link tokens**). |
| **SMTP** | **`SMTP_URL`** + **`SMTP_FROM`** for real **sign-in link** email; not required on laptop if you use logs/Mailhog. |
| **DNS for mail** | Often **SPF/DKIM** so **magic-link** messages are not spam—admin or whoever owns DNS. |
| **TLS + hostname** | HTTPS URL for the site; reverse proxy (nginx, Caddy, etc.) in front of Node. |
| **Health check** | Load balancer or platform should probe **`GET /api/health`** (or your chosen path). |
| **Secrets storage** | Env vars or secret manager—never commit `.env` with secrets. |
| **Backups** | Postgres backup/restore for production (and ideally staging). |
Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admin builds/pushes/runs the container with the env vars above.
### Ticket-by-ticket: admin involvement
| Ticket | Need server admin? | What for |
| ------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 12 | **No** | Docs and app code only. |
| 3 | **No** to build/test; **Yes** when **magic-link email** must work on a **deployed** env | Real **SMTP** + DNS on staging/prod (same as table above). |
| 48 | **No** | Local or staging URL is still “your” deploy—admin only if that URL is on their infra. |
| 9 | **No** to implement; **Yes** when **production** uses multiple instances or read-only FS | **Default** is external RUM/log drain; Postgres vitals only if ops explicitly wants one datastore—may need vendor keys for SaaS. |
| 10 | **No** to code | Same deploy pipeline as the rest of the app. |
| 11 | **Maybe** | Whoever owns **Gitea runners**: can they run Postgres in CI? Not the same as production server, but often the same “infra” person. |
| 12 | **Yes—this is the handoff ticket** | You (or admin) write **`docs/ops-backend-deploy.md`** so deploy steps are explicit; **you need admin input** to fill in hostnames, DB provider, SMTP, backup policy. |
### One-line summary
**You only need the server admin when you move off your laptop to a shared staging/production host**—for database, secrets, TLS, SMTP/DNS, migrations on that DB, health checks, and backups. Until then, **Tickets 18 are unblocked** with Docker Compose locally.
---
## Ticket 1 — Align `docs/backend-roadmap.md` with the current codebase
**Depends on:** nothing.
**Goal:** Remove stale statements so the roadmap matches reality and stays a trustworthy reference until you delete it.
**Context:** Section 1 still says there is no DB and only web-vitals API; the app now has Postgres models, auth, drafts, rules, templates API, etc.
**Implementation:**
1. Rewrite **§1 Where we are** to list: Prisma + Postgres, existing `app/api/*` routes, create-flow persistence (anonymous `localStorage` + optional server draft PUT when sync is on), web-vitals still file-based.
2. In **§9 Build order** (build steps were renumbered from old §5), mark what is **operator/manual**, what is **already shipped in the repo**, and what is **still product/frontend** (publish wiring, templates in UI, etc.).
3. Add **HTTP API (implemented in repo)** — table mirroring [CONTRIBUTING.md](CONTRIBUTING.md), plus note for `/api/web-vitals`.
**Acceptance criteria:**
- [x] A new contributor reading only the roadmap does not think the backend is unbuilt.
- [x] **§13 Optional later** (old §9) unchanged in intent — optional Redis, session-library spike, draft versioning, standalone API, OpenAPI, fourth env.
**Status:** [CR-72](https://linear.app/community-rule/issue/CR-72/backend-align-docsbackend-roadmapmd-with-current-codebase) **Done**.
**Files:** [docs/backend-roadmap.md](docs/backend-roadmap.md) only.
---
## Ticket 2 — Formalize `CreateFlowState` and validate API payloads
**Depends on:** Ticket 1 (optional but keeps docs honest).
**Goal:** Replace the open `[key: string]: unknown` shape in [app/(app)/create/types.ts](app/(app)/create/types.ts) with real fields (or nested objects) agreed with design/product, and validate JSON on the server for drafts and publish.
**Context:** `PUT /api/drafts/me` and `POST /api/rules` accept loose objects today; oversized or malformed payloads are a stability and security concern.
**Implementation:**
1. Document intended fields per create-flow step (can start minimal: e.g. `title`, `sections`, `stakeholders` placeholders) in `CreateFlowState`.
2. Add **Zod** (or reuse **Ajv** if you prefer consistency with [lib/validation.ts](lib/validation.ts)) schemas:
- `createFlowStateSchema` for draft `payload`.
- `publishedRuleDocumentSchema` for `document` on `POST /api/rules`.
3. In [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts) and [app/api/rules/route.ts](app/api/rules/route.ts), parse with schema; on failure return `400` with a small `{ error, details? }` body.
4. Enforce a **max payload size** (e.g. reject bodies > 512KB) via route handler check or Next config if applicable.
**Acceptance criteria:**
- [x] TypeScript reflects the real shape of `CreateFlowState` (no unnecessary `unknown` for known keys).
- [x] Invalid draft/publish requests return 400, not 500.
- [x] Unit tests for schemas (Vitest) or route tests with MSW.
**Status:** [CR-73](https://linear.app/community-rule/issue/CR-73/backend-formalize-createflowstate-validate-draftpublish-api-payloads) **Done**.
**Files:** [app/(app)/create/types.ts](app/(app)/create/types.ts), [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/server/validation/](lib/server/validation/) (Zod + plain-JSON checks), [package.json](package.json) (`zod`).
**Note:** Repo-wide **API error JSON shape** and **request-id logging** are **Ticket 13 / CR-84**—coordinate 400 response bodies with that issue so validation errors match the agreed `{ error: { code, message } }` pattern.
---
## Ticket 3 — Email magic-link sign-in UI (end-to-end with existing APIs)
**Depends on:** Ticket 2 (soft dependency: types help name fields you might store post-login; can start in parallel if needed).
**Server / admin:** **Not required** to build and test (Mailhog or verify URL in server logs locally). **Required** when **magic-link email** must work on **staging/production**: admin provides **SMTP** + usually **DNS (SPF/DKIM)** and sets env on the host (see top table). **Residual:** links in email use the app origin—reverse proxy / `Host` must match the URL users open.
**Goal:** Let a user request a **sign-in link** and complete sign-in in the browser using existing endpoints.
**Context:** APIs: `POST /api/auth/magic-link/request`, `GET /api/auth/magic-link/verify`, `GET /api/auth/session`, `POST /api/auth/logout`. Prisma: `MagicLinkToken`. Client: [`requestMagicLink`](lib/create/api.ts).
**Implementation (shipped):**
1. **`/login`** route **and** **header modal** — primary **Log in** entry is [`AuthModalProvider`](app/contexts/AuthModalContext.tsx) + [app/components/modals/Login/](app/components/modals/Login/); [app/(app)/login/page.tsx](app/(app)/login/page.tsx) (solid shell, `usePortal={false}`) remains for verify **error** redirects and bookmarks.
2. Flow: email → “Send link” → user opens link (email, Mailhog, or dev log) → `GET /api/auth/magic-link/verify?token=...` sets session and redirects; optional `next` for post-login path.
3. Surface API errors: invalid email, 429 `retryAfterMs`, expired/invalid token, network failure (accessible copy).
4. Ensure `fetch` calls use `credentials: "include"` where needed (see [lib/create/api.ts](lib/create/api.ts)).
5. **Dev:** without `SMTP_URL`, verify URL is logged; with Mailhog, use [docker-compose.yml](docker-compose.yml) and `SMTP_URL=smtp://localhost:1025`.
6. **Marketing header:** When signed in (`fetchAuthSession`), **Log in** becomes **Profile** linking to [`/profile`](app/(app)/profile/page.tsx) (placeholder until Ticket 15 / CR-86). Implemented in [TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx) + [TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx).
**Acceptance criteria:**
- [x] Happy path: user completes magic-link verify and `GET /api/auth/session` returns `user` in the same browser session.
- [x] Keyboard + screen-reader friendly forms (labels, errors associated with fields).
- [x] No secrets in client bundle.
- [x] Header shows **Profile** → placeholder `/profile` when session present; **Log in** when anonymous (opens modal, not only `/login`).
**Status:** [CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done) **Done** for shipped UI/APIs. **Residual checklist** below: repo doc items are **done**; use Linear (CR-74 or child issue) to track **per-environment** staging URL checks.
**Files:** [app/(app)/login/](app/(app)/login/), [app/(app)/profile/](app/(app)/profile/) (placeholder), [app/components/modals/Login/](app/components/modals/Login/), [messages/en/pages/login.json](messages/en/pages/login.json), [messages/en/pages/profile.json](messages/en/pages/profile.json), [messages/en/components/header.json](messages/en/components/header.json), [app/components/navigation/TopNav/TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx), [app/components/navigation/TopNav/TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx), [lib/create/api.ts](lib/create/api.ts), [app/api/auth/magic-link/request/route.ts](app/api/auth/magic-link/request/route.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), [prisma/schema.prisma](prisma/schema.prisma) (`MagicLinkToken`), [lib/server/mail.ts](lib/server/mail.ts). Onboarding: [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example).
### Residual / before CR-75 (create-flow session UI)
**Intent:** [Ticket 4](#ticket-4--session-affordances-in-the-create-flow-signed-in-state--sign-out) (**CR-75**) needs a reliable signed-in story across marketing + `/create`. Below: what is **done in repo** vs what to **verify per environment**.
1. **Contributor / onboarding****Done:** [CONTRIBUTING.md](CONTRIBUTING.md) API table and sign-in section describe **magic-link** request/verify, dev log URL, and Mailhog. [`.env.example`](.env.example) comments match.
2. **Smoke checklist****Done:** **Email magic link (sign-in)** in [CONTRIBUTING.md](CONTRIBUTING.md); build-order §9 in [docs/backend-roadmap.md](backend-roadmap.md) includes the same happy path + session check.
3. **Staging / production URLs****Verify on each deploy:** emails use `request.nextUrl.origin`; confirm reverse proxy and **`Host`** so links in mail match the public site (CONTRIBUTING + roadmap §9 spell this out).
4. **Docs alignment****Done:** [docs/backend-roadmap.md](backend-roadmap.md) and this doc treat magic link as primary; CR-72/CR-73 schema work is not a blocker for CR-75.
---
## Ticket 4 — Session affordances in the create flow (signed-in state + sign out)
**Depends on:** Ticket 3.
**Goal:** In `/create/*`, **Exit** / **Save & Exit** (from `select` onward for signed-in users) is the only top-nav chrome—no email or Sign out in the create shell. **Anonymous:** progress in **`create-flow-anonymous`** localStorage; **Exit** opens the global **Save your progress?** auth modal (magic link + `?syncDraft=1` return); after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) **PUT**s to `/api/drafts/me` when sync is on. **Signed-in:** **Save & Exit** **PUT**s via [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC`**. **Sign out** for QA lives on **[ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx)**. Site **Log in** opens the same modal overlay ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)), not only `/login`.
**Context:** **`saveDraftOnExit`** is gated on **session + step ≥ select**. Layout **`fetchAuthSession`** drives anonymous vs authenticated persistence and exit behavior. **Save & Exit** styling: Figma [20907:212637](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20907-212637). Save-progress exit modal: Figma `22398:23743`.
**Implementation (repo):**
1. [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx): session + `enableAnonymousPersistence`; anonymous exit → `openLogin({ variant: 'saveProgress', nextPath })`; signed-in exit → `useCreateFlowExit`.
2. [CreateFlowTopNav](app/components/utility/CreateFlowTopNav/): i18n [`messages/en/create/topNav.json`](messages/en/create/topNav.json); logo + Share/Export/Edit (completed) + Exit/Save & Exit only.
3. [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts): `saveDraftToServer` when sync + signed in; `clearState` + home.
4. [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx): optional anonymous localStorage mirror via `enableAnonymousPersistence`.
5. **QA:** [ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx) Sign out when session present.
**Acceptance criteria:**
- [x] Completed step still works; **Save & Exit** gating uses session + step (not conflated with `completed` only).
- [x] Signed in + sync: Save & Exit persists server-side; anonymous: localStorage + exit modal + transfer after magic link. Sign out on profile clears session. _(Re-verify on staging/prod as needed.)_
**Files:** [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/components/utility/CreateFlowTopNav/](app/components/utility/CreateFlowTopNav/), [app/(app)/create/context/CreateFlowContext.tsx](app/(app)/create/context/CreateFlowContext.tsx), [messages/en/create/topNav.json](messages/en/create/topNav.json), [app/(app)/profile/ProfilePageClient.tsx](app/(app)/profile/ProfilePageClient.tsx).
---
## Ticket 5 — Harden server draft sync (UX + edge cases)
**Depends on:** Tickets 24.
**Goal:** Server draft **PUT** path is production-grade when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (Save & Exit, post-login transfer from anonymous draft).
**Context:** Auto-hydrate / debounced autosave component was removed; signed-in resume uses `GET /api/drafts/me` in the create layout.
**Implementation:**
1. **Hydration:** **Done:** [SignedInDraftHydration](app/(app)/create/SignedInDraftHydration.tsx) + [messages/en/create/draftHydration.json](messages/en/create/draftHydration.json); skips `?syncDraft=1` / transfer-pending (PostLogin owns that). Wired in [layout](app/(app)/create/layout.tsx).
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/(app)/create/utils/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
3. **Save failures (API surface):** **Done (CR-76):** [saveDraftToServer](lib/create/api.ts) returns `SaveDraftResult` with parsed API `message`; wired in [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts) and [PostLoginDraftTransfer](app/(app)/create/PostLoginDraftTransfer.tsx).
4. **Save failures (UX):** **Done (CR-76):** Dismissible banner with server `message` (no second confirm to leave); post-login transfer shows reason; unit tests in `tests/unit/saveDraftToServer.test.ts`. Retry/backoff remains optional.
5. **Tests:** `saveDraftToServer` unit tests; [draftHydrationUtils](lib/create/draftHydrationUtils.ts) unit tests. Playwright against Next standalone + route mocks for `/api/auth/session` was flaky here; cover hydration with **manual QA** (signed in + sync on + server draft) or add a future E2E with a dedicated auth fixture.
**Acceptance criteria:**
- [x] No silent data loss when server save fails (user sees reason in banner; stays in flow to retry Save & Exit or leave via e.g. logo).
- [x] User understands when server draft replaced local state (if applicable) — conflict `window.confirm` when both browser anonymous draft and account draft exist; otherwise silent apply of single source.
**Files:** [lib/create/api.ts](lib/create/api.ts), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/(app)/create/PostLoginDraftTransfer.tsx](app/(app)/create/PostLoginDraftTransfer.tsx), [app/(app)/create/SignedInDraftHydration.tsx](app/(app)/create/SignedInDraftHydration.tsx), [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx), tests under `tests/`.
---
## Ticket 6 — Wire “Publish rule” from the create flow to `POST /api/rules`
**Depends on:** Tickets 24 (Ticket 5 optional).
**Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts).
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on the `final-review` / `completed` steps (see [app/(app)/create/screens/CreateFlowScreenView.tsx](app/(app)/create/screens/CreateFlowScreenView.tsx) and `app/(app)/create/screens/`) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
**Implementation:**
1. Map `useCreateFlow().state``title` / `summary` / `document` (document likely mirrors [CommunityRuleDocument](app/components/sections/CommunityRuleDocument/) shape or raw JSON).
2. Call `publishRule` on explicit user action (“Publish” / “Finalize”) or on transition to `completed` (product decision—prefer explicit button to avoid double-submit).
3. Handle **401**: redirect or modal to sign-in (Ticket 3).
4. Success: navigate to `completed` with rule id in query or state; optional confetti per design.
**Acceptance criteria:**
- [ ] Published row appears in Postgres (`PublishedRule`) and `GET /api/rules` lists it.
- [ ] User sees clear success/failure.
**Files:** relevant `app/(app)/create/*/page.tsx`, [lib/create/api.ts](lib/create/api.ts) if request shape changes, types from Ticket 2.
---
## Ticket 7 — Seed `RuleTemplate` data and document how to re-run
**Depends on:** none (API exists at [app/api/templates/route.ts](app/api/templates/route.ts)).
**Goal:** Curated templates exist in DB for recommendations (v1 = static curated list, no ML).
**Not in v1 (this ticket):** **Spreadsheet-authored matrices**, multi-axis **facet filtering**, or **ranked** recommendations from user answers — that is **Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** after the flat list ships.
**Implementation:**
1. Add [Prisma seed](https://www.prisma.io/docs/guides/migrate/seed-database): `prisma/seed.ts` with `upsert` on `slug` for idempotent runs.
2. In [package.json](package.json), set `"prisma": { "seed": "tsx prisma/seed.ts" }` or `node --loader ts-node/esm` per your preference.
3. Seed 310 rows aligned with marketing copy today ([messages/en/components/ruleStack.json](messages/en/components/ruleStack.json) or home cards) — `title`, `category`, `description`, `body` JSON, `sortOrder`, `featured`.
4. Document: `npx prisma db seed` in [CONTRIBUTING.md](CONTRIBUTING.md).
**Acceptance criteria:**
- [ ] `GET /api/templates` returns non-empty `templates` after seed on empty DB.
- [ ] Re-running seed does not duplicate rows.
**Files:** `prisma/seed.ts`, [package.json](package.json), [CONTRIBUTING.md](CONTRIBUTING.md).
---
## Ticket 8 — Load rule templates from the API in the UI
**Depends on:** Ticket 7.
**Goal:** Home or create entry surfaces use live template data instead of only static i18n JSON.
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and create entry surfaces reference future template work. Wizard URLs are static segments under `app/(app)/create/`; see [`docs/create-flow.md`](create-flow.md) and **Ticket 17** for the canonical custom flow.
**Implementation:**
1. Add a small client or server data fetch to `GET /api/templates` (RSC `fetch` with cache tags, or client `useEffect` with loading skeleton—match existing data-fetch patterns in the app).
2. Map API rows to existing card components; keep i18n for chrome strings (“See all templates”).
3. Empty state: if API returns `[]`, fall back to static copy or hide section per design.
**Acceptance criteria:**
- [ ] Changing a template row in Prisma Studio reflects after refresh (or revalidate).
- [ ] No layout shift regression on LCP-critical pages (use skeletons).
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), create-flow entry routes under [app/(app)/create/](app/(app)/create/), possibly new `lib/templates/fetchTemplates.ts`.
**Follow-up:** **Ticket 16** — dynamic recommendations from authoring spreadsheets and create-flow answers.
---
## Ticket 16 — Template recommendation matrix + spreadsheet ingestion
**Depends on:** Tickets 78 (templates exist in DB and UI can fetch them). Can overlap **Ticket 6** (create flow) for wizard steps that POST answers.
**Goal:** Support **dynamic** template selection driven by **authoring spreadsheets** (e.g. Excel / Google Sheets exported to `.xlsx`): each **row** is a template variant with long-form copy (title, description, principles, steps, objections); **columns** encode **matching dimensions** (group size bands, organization type, location, maturity, etc.) with symbols or weights (✓/✗, 01 scores). The create flow (or home) should **narrow or rank** options from **user-supplied facets** or a short questionnaire.
**Context:** The current [`RuleTemplate`](prisma/schema.prisma) model is a **flat** list (`slug`, `title`, `category`, `description`, `body` JSON). It does **not** model dimension columns, matrix versioning, or import from sheets. Example product shape: a “Decision-making” workbook → many governance patterns, each row tied to applicability across org context.
**Implementation (phased — product can stop after any phase):**
1. **Authoring contract:** Document required columns / sheet tabs (per domain: decision-making, meetings, etc.), validation rules, and how ✓/✗ or numeric cells map to API filters or scores.
2. **Storage:** Either extend `RuleTemplate` / `body` with a structured `recommendationMatrix` blob **or** add normalized tables (`TemplateDimension`, `TemplateFacetValue`, `TemplateApplicability`) — pick based on query needs and reporting.
3. **Import:** Script or internal admin path: `.xlsx` → parse (e.g. `xlsx` / SheetJS) → validate → upsert DB rows or generate seed JSON checked into repo. **Default:** batch job on export, **not** live Sheets API in prod unless explicitly required.
4. **API:** Extend `GET /api/templates` with optional query params (`?facet.orgType=nonprofit&facet.size=6-12`) **or** add `POST /api/templates/recommend` with a JSON body of answers; return ranked `templates` + optional `scores` / `reasons` for UI.
5. **UI:** Create-flow step(s) collect facets; call API; prefill `CreateFlowState` or document JSON from chosen rows `body`.
**Acceptance criteria:**
- [ ] Importing an updated workbook (or running the importer) changes recommendations without hand-editing Prisma rows in Studio.
- [ ] API behavior is documented (params or POST body) and covered by tests for at least one reference matrix.
- [ ] Invalid / partial facet combinations degrade gracefully (empty list vs fallback featured templates).
**Files (expected):** `prisma/schema.prisma`, `lib/templates/*` or `scripts/import-templates-xlsx.ts`, `app/api/templates/*`, create-flow pages, tests.
**Linear:** [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) (**Backlog**). **Parallel** to much of the core chain; **blocked** only by having **CR-78**/**CR-79** far enough along that a template list exists (or stub rows).
---
## Ticket 17 — Canon custom create-rule wizard (routes, resume, progress) + docs
**Depends on:** none for documentation; soft optional **CR-73**, **CR-76**, **CR-77** for payload/resume/publish alignment.
**Goal:** Establish the **official custom** create-rule flow (ordered steps, URLs, persistence, entry points, **Figma three-stage framing**) in repo docs and close gaps between that spec and the implementation (routing clutter, progress UI, step source of truth, resume vs URL).
**Context:** Step order lives in [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts). Wizard screens render from [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) plus [`CREATE_FLOW_SCREEN_REGISTRY`](app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node + layout family per slug). [`docs/create-flow.md`](create-flow.md) is the **canonical** URL/persistence summary: **Create Community** (eight semantic steps ending at `review`) → **Create Custom CommunityRule****Review and complete**. **Full create-from-template** will likely use **additional route(s)** when product defines it; **`/create/review-template/[slug]`** remains auxiliary preview only. **Template → `final-review` or mid-wizard prefill** is **out of scope** here (future ticket); `/create/informational?template=` is a **no-op** until then.
**Implementation:**
1. Keep [`docs/create-flow.md`](create-flow.md) in sync with product/Figma (stage ↔ step mapping, future template routes).
2. ~~Remove legacy [`app/(app)/create/[step]/page.tsx`](app/(app)/create/[step]/page.tsx)~~ — replaced by [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) with real screens; unknown slugs `notFound()`.
3. Unify **step source of truth**: URL via [`useCreateFlowNavigation`](app/(app)/create/hooks/useCreateFlowNavigation.ts) vs unused [`CreateFlowContext`](app/(app)/create/context/CreateFlowContext.tsx) `currentStep` — pick one model; align [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) / draft payload if needed.
4. **Resume:** After [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx), decide redirect to `/create/${state.currentStep}` vs stay on current URL; test or document.
5. Wire [`CreateFlowFooter`](app/components/utility/CreateFlowFooter/) `ProportionBar` to step progress from `FLOW_STEP_ORDER` (and `review-template` / `completed` exceptions per design); optional **two-level progress** (stage + step within stage) when design specifies.
6. When Figma hands off, surface **stage labels** in create shell (top nav, footer, or step chrome) using the mapping in `create-flow.md`.
**Acceptance criteria:**
- [ ] [`docs/create-flow.md`](create-flow.md) matches shipped behavior or lists known gaps, including **Figma three-stage** mapping and **future template route** note.
- [ ] No misleading dynamic step placeholder for valid wizard URLs.
- [ ] Footer progress reflects step index **or** doc/issue records a deliberate deferral with design sign-off.
- [ ] Hydration + `currentStep` behavior is verified (redirect vs stay).
- [ ] `?template=` documented as deferred; no implied “template customize → full wizard” parity.
**Files:** [`docs/create-flow.md`](create-flow.md), [`app/(app)/create/`](app/(app)/create/), [`app/components/utility/CreateFlowFooter/`](app/components/utility/CreateFlowFooter/), optionally [`docs/backend-roadmap.md`](backend-roadmap.md) §12 cross-links.
**Linear:** [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) (**Backlog**). **Parallel** to templates (78) and publish (6); not part of **CR-72 → CR-83**.
---
## Ticket 9 — Persist web vitals outside `.next` (prefer external RUM)
**Depends on:** none (orthogonal).
**Server / admin:** **Not required** to implement. **Relevant** when production is **multi-instance** or **read-only filesystem**; external tools may need **vendor API keys** in env.
**Goal:** [app/api/web-vitals/route.ts](app/api/web-vitals/route.ts) stops relying on ephemeral files under `.next/web-vitals` in production.
**Context:** Multi-instance / Docker loses file-based metrics. [docs/backend-roadmap.md](backend-roadmap.md) §7: **default** is **external** analytics or log drain—keep product Postgres for product data.
**Implementation (pick one — prefer A or B first):**
- **A (preferred):** Integrate **external RUM / logging** (host metrics, Vercel Web Analytics, OpenTelemetry export, Datadog, etc.); stop or thin local aggregation; `app/(admin)/monitor/page.tsx` links out or shows reduced scope.
- **B:** Forward events from the route to a **log drain** or APM; trim custom dashboard if redundant.
- **C (fallback only):** New Prisma model `WebVitalEvent` + migrate + read path in monitor—**only** if ops explicitly chooses a single-store tradeoff (document why).
**Acceptance criteria:**
- [ ] Production with read-only filesystem does not break vitals collection path.
- [ ] Monitor page still useful or intentionally replaced with a doc link.
**Files:** [app/api/web-vitals/route.ts](app/api/web-vitals/route.ts), [app/(admin)/monitor/](<app/(admin)/monitor/page.tsx>) (adjust paths as needed), optionally `prisma/schema.prisma` **only if** option C.
---
## Ticket 10 — Public rule detail (optional product scope)
**Depends on:** Ticket 6.
**Goal:** Shareable link for a published rule.
**Note:** Complements **Ticket 15** profile cards: users can open a **public** detail URL from a rule listed on their dashboard; the profile page does **not** replace this ticket.
**Implementation:**
1. Add `GET /api/rules/[id]/route.ts` returning `{ rule }` or 404 (public read; no secrets).
2. Add `app/(marketing)/rules/[id]/page.tsx` (or under `create` if private) rendering `document` JSON with existing document components.
3. Consider soft-delete flag later; out of scope unless product requires hide.
**Acceptance criteria:**
- [ ] UUID/cuid from Ticket 6 opens a readable page for anonymous users.
- [ ] Invalid id returns 404.
**Files:** new route handler, new page, optional layout.
**Linear:** [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional). **Related in Linear:** [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) (Ticket 15 — profile cards linking to public detail).
---
## Ticket 11 — CI: database migration smoke (optional, runner-dependent)
**Depends on:** existing [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml).
**Server / admin:** **Not production server**—but you may need whoever runs **Gitea/self-hosted runners** to allow **Postgres in CI** (Docker service / sidecar) or to accept a **manual migrate** process documented instead.
**Goal:** Catch broken SQL migrations before merge.
**Context:** Lint job already runs `prisma validate` with a dummy `DATABASE_URL`. **Migrate** needs a real Postgres reachable from the runner.
**Implementation:**
1. If Gitea runners support **Docker sidecar** or **postgres service**, add a job: start Postgres, set `DATABASE_URL`, `npx prisma migrate deploy`, then run a minimal test that hits `/api/health` with DB connected (may require `next build` + short `next start` + curl).
2. If **macOS self-hosted** runners cannot run service containers easily, document in CONTRIBUTING: “run `migrate deploy` against staging before prod” and keep validate-only in CI.
**Acceptance criteria:**
- [ ] Broken migration fails CI **or** documented alternative process is explicit.
**Files:** [.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml), [CONTRIBUTING.md](CONTRIBUTING.md).
---
## Ticket 12 — Staging / production runbook (operator checklist)
**Depends on:** Tickets 18 complete enough to deploy a vertical slice.
**Server / admin:** **This is the main ticket where you need the admin.** You draft the runbook; **admin fills in real hostnames, DB endpoint, SMTP, backup tooling, and who runs `migrate deploy`.** Without their input, you cannot complete production-ready deploy steps.
**Goal:** Single doc for admin: env vars, TLS, DB backups, migrations, Docker, SMTP, health checks.
**Implementation:**
1. Add `docs/ops-backend-deploy.md` (or similar) with numbered steps:
- Required env: `DATABASE_URL`, `SESSION_SECRET`, `SMTP_URL`, `SMTP_FROM`, optional `NEXT_PUBLIC_ENABLE_BACKEND_SYNC`.
- `docker compose` vs `Dockerfile` deploy; `prisma migrate deploy` before traffic.
- Reverse proxy: `GET /api/health` for LB health.
- Backups and restore drill for Postgres.
- SMTP DNS (SPF/DKIM).
2. Cross-link [docs/backend-roadmap.md](docs/backend-roadmap.md) §11 (environments) and §8 (migrations policy); note **never rewrite applied migrations** and where application logs go.
**Acceptance criteria:**
- [ ] Someone who did not write the code can deploy and roll back migrations with only the doc.
**Files:** new `docs/ops-backend-deploy.md`.
---
## Ticket 13 — API error contract + structured logging
**Depends on:** Ticket 2 (validation work defines many 400s).
**Server / admin:** None.
**Goal:** Standardize JSON errors and lightweight observability per [docs/backend-roadmap.md](backend-roadmap.md) §7.
**Implementation:**
1. Document target shape `{ error: { code, message }, details? }` and map validation failures into `details` where useful.
2. Add a small **route helper** or wrapper: generate or forward **`x-request-id`**, log errors with `lib/logger` + id.
3. Migrate high-traffic or auth routes first; follow-up pass for remaining `app/api/*`.
**Acceptance criteria:**
- [ ] At least auth + draft + rules routes return the agreed shape for new code paths.
- [ ] Errors in logs include request id when available.
**Files:** `lib/server/` (new helper), selected `app/api/**/route.ts`, optional tests.
**Linear:** [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) (**CR-73** Done — ready to pick up).
---
## Ticket 14 — Custom session lifecycle (rotation, cleanup, policy)
**Depends on:** Ticket 4 (session visible in create flow).
**Server / admin:** None for implementation; production cron may need admin if cleanup runs as a job.
**Goal:** Make custom Prisma sessions maintainable: rotation, invalidation policy, expired-row cleanup—per [docs/backend-roadmap.md](backend-roadmap.md) §45.
**Implementation:**
1. **Policy:** On **new sign-in** (magic-link verification / session creation), decide whether to **delete other `Session` rows** for that user (single active session) or allow multiple devices (document choice).
2. **Rotation (optional v1.1):** Issue new token on privilege-sensitive actions if product requires.
3. **Cleanup:** Delete or mark expired sessions (scheduled job, or prune on read with occasional batch).
4. **Docs:** Add short ADR or comment block in `lib/server/session.ts`.
**Acceptance criteria:**
- [ ] Documented behavior matches implementation.
- [ ] Expired sessions do not accumulate unbounded in production over months.
**Files:** [lib/server/session.ts](lib/server/session.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), optional `prisma` migration if new columns (unlikely).
**Linear:** [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) (**unblocked** — **CR-75** Done).
---
## Ticket 15 — Profile dashboard + account (Figma profile)
**Depends on:** Ticket 3 (auth), **Ticket 4** (session in UI), **Ticket 6** (publish so users have rules to list). Soft optional: Tickets 78 for “create from template” CTA parity.
**Goal:** Signed-in **profile** experience matching [Figma — Profile](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069): **Your CommunityRules** (list **own** published rules), **duplicate** / **delete** per rule, CTAs into create flow (custom + from template), **logout** (existing API), **delete account** (policy + API + confirmation UX).
**Out of scope for this ticket**
- **Change your account email** (shown in Figma options): **deferred**—no backend in this slice. Product may **hide** the row, show **“Coming soon,”** or backlog until a **future ticket** (verified email change, conflicts, sessions).
- **`displayName` / new `User` fields:** not required—use **static** welcome copy, generic greeting, or **email local-part in UI only** until a later schema/product decision.
**Context:** Today `GET /api/rules` is a **public** list of all published rules; there is no authenticated **my rules** endpoint, no owner **DELETE** / **duplicate**, and no **delete user** API. See [docs/backend-roadmap.md](backend-roadmap.md) §1 “profile / account — not implemented yet” and §6.
**Implementation (sketch):**
1. **API:** Authenticated route(s) to list rules **where `userId` = session user**; owner-only `DELETE` (and duplicate via `POST` reuse or dedicated handler); `DELETE` user (or equivalent) with explicit Prisma policy—cascade vs orphan `PublishedRule` (today `onDelete: SetNull` on user) and cleanup of `Session` / `RuleDraft`.
2. **UI:** Marketing route (e.g. `/profile`), rule cards (title, summary, artwork from `document` as needed), **IN PROGRESS** badge per roadmap §4 (derive from JSON / future `status` / UI-only).
3. **Nav:** Link from header when signed in if design requires.
4. **i18n** for strings; legal/product review for **delete account** copy.
**Acceptance criteria:**
- [ ] Signed-in user sees **only their** published rules (not the global public list).
- [ ] Duplicate and delete actions work for **owner** only; errors are clear.
- [ ] Logout still works from profile context.
- [ ] Delete account flow matches agreed policy and is confirmed in UI.
- [ ] No verified **email change** shipped in this ticket; Figma row handled per product (hide/disabled/backlog).
**Files:** new `app/` routes and components, `app/api/rules/...` (or new segment handlers), [lib/create/api.ts](lib/create/api.ts) as needed, [prisma/schema.prisma](prisma/schema.prisma) only if account-delete policy requires schema tweaks, [messages/en/](messages/en/) for copy.
**Linear:** [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) (**Backlog**). **Blocked by** **CR-77** (publish) only — **CR-75** Done. **Related:** [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) (public rule detail for deep links from profile cards). **Not** part of the sequential **CR-72 → CR-83** chain—parallel after publish + session, similar to CR-84/CR-85.
---
## Summary order
| Order | Ticket | Short name |
| ----: | ------ | --------------------------------- |
| 1 | 1 | Refresh backend-roadmap |
| 2 | 2 | CreateFlowState + API validation |
| 3 | 3 | Magic-link sign-in UI |
| 4 | 4 | Create flow session UI |
| 5 | 5 | Draft sync hardening |
| 6 | 6 | Publish wiring |
| 7 | 7 | Template seed |
| 8 | 8 | Templates in UI |
| 9 | 9 | Web vitals persistence |
| 10 | 10 | Public rule detail (optional) |
| 11 | 11 | CI migrate smoke (optional) |
| 12 | 12 | Ops runbook |
| 13 | 13 | API errors + request-id logging |
| 14 | 14 | Session lifecycle + cleanup |
| 15 | 15 | Profile + account (Figma profile) |
| 16 | 16 | Template matrix + xlsx ingestion |
| 17 | 17 | Canon create-flow (custom path) |
Tickets **1011** can be deferred without blocking the core “auth + drafts + publish + templates” vertical slice. **Ticket 16** is also **deferrable** until after **78** (flat template list + UI); it adds **spreadsheet-driven** recommendations and facet APIs. **Ticket 17** (**[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)**) canonizes the **custom** wizard in [`docs/create-flow.md`](create-flow.md) and tracks UX/code alignment (progress bar, resume URL, `[step]` cleanup); **parallel** to publish and templates. **Tickets 1314** are parallel to that chain (**CR-73** / **CR-75** prerequisites are **Done****CR-84** / **CR-85** are unblocked), not sequential after CR-83. **Ticket 15** is also **parallel** (blocked by **publish (CR-77)** once session/auth are shipped); Linear: **CR-86**.
---
## Linear (Community-rule team)
**Main chain:** **CR-72 → CR-83** (each blocks the next). **Parallel:** **CR-84** (**CR-73** Done — ready to pick up), **CR-85** (**CR-75** Done — ready to pick up), **CR-86** / Ticket 15 (blocked by **CR-77** publish only; **CR-75** Done), **CR-88** / Ticket 16 (template matrix + `.xlsx` ingestion — after **CR-78**/**CR-79**), **CR-89** / Ticket 17 (canon create-flow + implementation gaps), not in the CR-7283 sequence.
| Doc ticket | Linear | Title (short) |
| ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| 1 | [CR-72](https://linear.app/community-rule/issue/CR-72/backend-align-docsbackend-roadmapmd-with-current-codebase) | Align backend-roadmap |
| 2 | [CR-73](https://linear.app/community-rule/issue/CR-73/backend-formalize-createflowstate-validate-draftpublish-api-payloads) | CreateFlowState + API validation |
| 3 | [CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done) | Magic-link sign-in UI (Ticket 3; Done) |
| 4 | [CR-75](https://linear.app/community-rule/issue/CR-75/backend-create-flow-session-ui-sign-out-ticket-4-done) | Create flow session UI (Ticket 4; Done) |
| 5 | [CR-76](https://linear.app/community-rule/issue/CR-76/backend-harden-server-draft-sync-save-and-exit-post-login-transfer) | Draft sync hardening (PUT UX / errors) |
| 6 | [CR-77](https://linear.app/community-rule/issue/CR-77/backend-wire-publish-rule-from-create-flow-post-apirules) | Publish wiring |
| 7 | [CR-78](https://linear.app/community-rule/issue/CR-78/backend-prisma-seed-ruletemplate-document) | Template seed |
| 8 | [CR-79](https://linear.app/community-rule/issue/CR-79/backend-load-rule-templates-from-get-apitemplates-in-ui) | Templates in UI |
| 9 | [CR-80](https://linear.app/community-rule/issue/CR-80/backend-persist-web-vitals-outside-next-db-or-external-rum) | Web vitals (prefer external) |
| 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) |
| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | CI migrate smoke (optional) |
| 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops runbook / admin handoff |
| 13 | [CR-84](https://linear.app/community-rule/issue/CR-84/backend-api-error-contract-request-id-logging) | API errors + request-id logging |
| 14 | [CR-85](https://linear.app/community-rule/issue/CR-85/backend-custom-session-lifecycle-cleanup-invalidation-policy) | Session lifecycle + cleanup |
| 15 | [CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile) | Profile + account (Figma 22143:900069) |
| 16 | [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) | Template matrix + xlsx ingestion |
| 17 | [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) | Canon create-flow (custom wizard + docs) |
---
## Linear sync notes (CR-74 / CR-75)
**[CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done)** and **[CR-75](https://linear.app/community-rule/issue/CR-75/backend-create-flow-session-ui-sign-out-ticket-4-done)** are kept in sync with **Ticket 3** / **Ticket 4** above (magic link, `AuthModalProvider`, anonymous draft + transfer, etc.). **Residual:** staging/prod `Host` / magic-link URL verification (per-environment).
To refresh other issues from this doc, use Linear MCP `save_issue` or paste the matching **Ticket N** section into the issue body.
+262
View File
@@ -0,0 +1,262 @@
# Backend roadmap (reference)
Temporary working notes for building the backend. Safe to delete once the stack is stable.
---
## 1. Where we are
- **Next.js 16** single repo ([`package.json`](package.json)).
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/magic-link, session, drafts, rules, templates, web-vitals).
- **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.).
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per “Create rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-structure` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts).
- **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts) still use **file-based** storage under `.next` (not suitable for multi-instance production).
- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition.
### HTTP API (implemented in repo)
Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers live under `app/api/*/route.ts`.
| Method | Path | Purpose |
| ---------- | ------------------------------ | --------------------------------------------- |
| GET | `/api/health` | Liveness / DB check |
| GET | `/api/auth/session` | Current user or null |
| POST | `/api/auth/magic-link/request` | Send sign-in link email |
| GET | `/api/auth/magic-link/verify` | Validate token, set session cookie, redirect |
| POST | `/api/auth/logout` | Clear session |
| GET / PUT | `/api/drafts/me` | Load or save create-flow JSON (authenticated) |
| GET / POST | `/api/rules` | List or publish rules |
| GET | `/api/templates` | List curated templates |
**Product sign-in** uses **magic link** (`/api/auth/magic-link/*`).
**Also present (not in CONTRIBUTING table):** `POST` / `GET` [`/api/web-vitals`](../app/api/web-vitals/route.ts) — file-based store today; production path TBD (§7).
### HTTP API (profile / account — not implemented yet)
Planned for the signed-in profile/dashboard ([Figma profile frame](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069); [docs/backend-linear-tickets.md](backend-linear-tickets.md) Ticket 15; Linear **[CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile)**):
- Authenticated list of **own** `PublishedRule` rows (e.g. `GET /api/rules/me` or a strictly scoped query—**not** the same as public `GET /api/rules`).
- Owner-only **delete** and **duplicate** (clone) for published rules.
- **Delete account** (authenticated), with an explicit policy for drafts, sessions, and linked rules.
**Future (separate ticket):** **Change email** with verification (e.g. magic link to a new address, conflict handling)—**out of scope** for the profile milestone above.
---
## 2. What were building
**Step 1.** Treat this as **greenfield**: new **PostgreSQL** database and new tables. Do **not** migrate data from the old Community Rule backend.
**Step 2.** Keep the backend **inside this Next app**:
- HTTP handlers under `app/api/…`
- Shared server code under `lib/server/…`
**Step 3.** Use the old backend only as a **product hint** (passwordless email sign-in, saving rules, listing rules). Do **not** copy its Express layout or MySQL schema.
---
## 3. Stack choices
**Step 1.** Use **PostgreSQL** everywhere (local Docker, staging, production).
**Step 2.** Use **Prisma**`schema.prisma`, `npx prisma migrate dev` / `migrate deploy`.
**Step 3.** Add **SMTP** (or Mailhog locally) for **magic-link** sign-in email in deployed environments; when `SMTP_URL` is unset in dev, the app can log the **verify URL** to the console (same pattern as [`lib/server/mail.ts`](lib/server/mail.ts)).
**Step 4.** **Redis / queues / Kubernetes** — not required for v1. **Exception:** before running **multiple app instances**, plan a **shared rate-limit store** (often Redis) for **passwordless email (magic-link request)**; the current limiter is in-memory per process ([`lib/server/rateLimit.ts`](lib/server/rateLimit.ts)).
---
## 4. Data to model (first pass)
Plain-English entities (names can evolve):
| Area | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **User** | Identified by email after **magic link verification** (primary v1 path). An optional **display name** (or preferred name) could be added later for richer greetings; it does **not** block the profile page—no schema commitment in this roadmap pass alone. |
| **Session** | **Custom v1:** HttpOnly cookie; opaque token; **hash** stored in DB ([`lib/server/session.ts`](lib/server/session.ts)). Not NextAuth/Lucia. |
| **MagicLinkToken** | Short-lived **hashed** token for email sign-in links; optional `nextPath` for post-login redirect. |
| **RuleDraft** | **One** JSON blob per user (create-flow state). Schema already has **`updatedAt`**; no draft **versioning** or **multiple named drafts** in v1. |
| **PublishedRule** | Saved rule after publish (title, summary, document JSON). Profile UI badges such as **IN PROGRESS** may be **derived from `document` JSON**, a future `status` column, or UI-only—product decision when implementing Ticket 15. |
| **RuleTemplate** | Curated templates (slug, category, ordering, `body` JSON). **v1 API** lists rows for cards / create entry; **not** yet a recommendation engine (see below). |
**RuleTemplate — recommendation matrix (after v1 list):** Product may author templates in **spreadsheets** (e.g. one row per governance pattern, columns for **matching dimensions** such as group size, organization type, location, maturity, plus long-form fields for create-flow prefill). That implies: **normalized schema or versioned JSON** for dimensions × template fit (✓/✗, weights, or scores), an **import path** (export `.xlsx` / Sheets → validate → DB or build-time artifact), and **`GET /api/templates` (or a sibling route)** that accepts **user- or wizard-selected facets** and returns a **ranked or filtered** set. **Out of scope for first ship** of Tickets 78 (seed + display list); tracked as **Ticket 16** in [docs/backend-linear-tickets.md](backend-linear-tickets.md) and Linear **[CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)**. Prefer **batch import** over live Google Sheets API in production unless ops explicitly wants sync.
**Session follow-ups to implement or decide:** token **rotation** on sensitive events, whether **new login invalidates other sessions**, and **cleanup** of expired `Session` rows (job or lazy delete). Revisit a small auth library (e.g. Auth.js, Lucia) only if maintaining custom code becomes costly.
**RuleDraft future (not v1):** versioning, multiple drafts per user, easier corruption recovery—only if product needs them.
Align JSON shapes with `app/(app)/create/types.ts` as it matures.
---
## 5. Session and authentication (v1)
- **Decision:** **Custom** database-backed sessions + **email magic link**; cookies are **httpOnly**; session and magic-link tokens are hashed at rest.
- **Rate limiting (magic-link request):** **In-memory** is acceptable for a **single Node process**. It does **not** coordinate across instances—**add a shared limiter (e.g. Redis)** before horizontal scaling or serious abuse exposure.
- Do **not** treat “switch to NextAuth/Lucia” as required for v1; document the custom lifecycle above instead.
---
## 6. Authorization (v1)
Match the current API behavior; tighten as product evolves:
- **`GET /api/drafts/me` / `PUT /api/drafts/me`:** Authenticated user only; draft is **scoped to that user** (`userId`).
- **`POST /api/rules`:** Authenticated user only; rule is stored with **`userId`** (owner).
- **`GET /api/rules`:** **Public list** of published rules (metadata: id, title, summary, timestamps)—no auth required today. **Not** a private “my rules” feed unless you add a separate route later (see §1 “profile / account — not implemented yet” and Ticket 15).
- **Profile / owner scope (planned):** Authenticated **list own rules**, **delete own rule**, **duplicate own rule**—required for the signed-in dashboard in design; **v1 shipped handlers** may not include these until that work lands.
- **Delete account (planned):** Authenticated endpoint + UX to remove the user record per policy (cascade vs orphan `PublishedRule`, drafts, sessions)—Ticket 15. **Change email** is **not** part of that milestone; plan a **future ticket** for verified email updates.
- **v1 (shipped today):** No **editing** or **deleting** published rules via API in current handlers; no **sharing** or **collaborative ownership**—treat each rule as **owned by one user** until product defines more.
---
## 7. API responses, errors, and observability
**Error JSON (target):** Prefer a stable shape, e.g. `{ "error": { "code": "string", "message": "string" }, "details"?: ... }` for 4xx/5xx, instead of only `{ "error": "string" }`. Validation errors can map into `details`. Implement gradually in route handlers.
**Logging:** Use the shared [`lib/logger.ts`](../lib/logger.ts) where possible. Include a **request correlation id** (reuse `x-request-id` if present, else generate) on API routes and log it with errors so support can tie logs together.
**Metrics:** No vendor required for v1; optional later: request duration, error counts.
**Web vitals:** **Default** is **external** RUM or log drain (e.g. host analytics, Vercel Analytics, OpenTelemetry, SaaS APM)—keep **product Postgres** focused on product entities. Storing vitals in Postgres is an **explicit tradeoff** only if ops strongly wants a single datastore.
---
## 8. Prisma migrations policy
- **Never edit** migration files that have **already been applied** to **staging or production** (or any shared database). Fixing schema drift = **add a new migration**.
- **Local dev:** `prisma migrate dev` creates migrations; **deployed envs:** `prisma migrate deploy` before serving new code that depends on the schema.
---
## 9. Build order (implementation steps)
**Operator / local (always manual):** Steps 14 — env file, Docker Postgres, `npm ci`, `prisma migrate dev`, `npm run dev`.
**Backend behavior already in the repo:** Steps **510** match implemented Route Handlers and middleware (`lib/server/*`). **Step 11** (web vitals) is **not** production-ready (files under `.next`); treat as follow-up work aligned with §7.
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumption (flat list first), **canon create-flow alignment** (Ticket 17 / [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) — progress bar, resume URL, `[step]` cleanup; spec in [`docs/create-flow.md`](create-flow.md)), **spreadsheet-driven template recommendations** (Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion) — after v1 templates), **profile / my rules dashboard** (Ticket 15)—see §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
---
**Step 1.** Copy `.env.example` to `.env`. Set `DATABASE_URL` and secrets (see file comments).
**Step 2.** Start Postgres locally:
```bash
docker compose up -d postgres
```
**Step 3.** Install dependencies and apply migrations:
```bash
npm ci
npx prisma migrate dev
```
**Step 4.** Run the app:
```bash
npm run dev
```
**Step 5.** Confirm **health**: `GET /api/health` should return JSON.
**Step 6.** **Magic-link sign-in** (happy path):
1. `POST /api/auth/magic-link/request` with `{ "email": "you@example.com" }` (optional `"next"` for redirect after verify).
2. Open the link from email, Mailhog, or **server logs** when `SMTP_URL` is unset (dev).
3. Browser hits `GET /api/auth/magic-link/verify?token=...` (and optional `next=...`); response sets the session cookie and redirects.
4. `GET /api/auth/session` should show your user in the same browser.
**Before wiring create-flow session UI:** Confirm the same browser that completed verify gets `user` from `GET /api/auth/session` (cookie + same-site). On **staging/production**, magic-link emails embed the app origin—misconfigured **`Host`** or TLS termination can produce broken links; align reverse proxy with the public site URL.
**Step 7.** **Drafts**: With a session, `GET /api/drafts/me` and `PUT /api/drafts/me` with `{ "payload": { ... } }` (create flow state object).
**Step 8.** **Publish**: `POST /api/rules` with `{ "title", "summary?", "document" }`.
**Step 9.** **Templates** (when ready): seed `RuleTemplate` rows; `GET /api/templates` is implemented.
**Step 10.** **Frontend draft sync:** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` in `.env` so **Save & Exit** and **post-login anonymous → account transfer** can **PUT** `/api/drafts/me`. Without sync, drafts are **not** written to the server (anonymous progress still lives in `localStorage` only).
**Step 11.** **Web vitals:** Move off `.next` files—**prefer an external analytics or logging pipeline** (see §7). Use Postgres for vitals only as a deliberate ops choice.
---
## 10. Security checklist
- **HTTPS** in staging/production; session cookie **Secure**.
- **Rate-limit** magic-link **request** — in-memory OK for one instance; **shared store before multi-instance** (see §5).
- **Hash** magic-link tokens and session tokens before storing; short **magic-link** TTL (align with implementation, e.g. 15 minutes).
- **Secrets** only in env / secret store — never commit `.env` with real values.
- **CORS:** prefer **same-origin** `/api/*`; if cross-origin, configure CORS and CSRF carefully.
---
## 11. Environments
| Environment | Purpose | Notes |
| -------------- | ------------------------------- | --------------------------------------------------------------------- |
| **Local** | Daily development | Docker Compose: Postgres + optional Mailhog (`docker compose up -d`). |
| **Staging** | Rehearse deploys and migrations | Match prod as closely as possible; test SMTP. |
| **Production** | Users | Backups, monitoring, migration job before traffic. |
**Optional QA:** Run automated tests against an **ephemeral** database in CI instead of maintaining a fourth long-lived server.
**Admin / infra (coordinate with whoever runs the server):**
1. TLS certificates and hostnames.
2. PostgreSQL backups and restore drill.
3. SMTP DNS (SPF, DKIM).
4. Health check URL for reverse proxy (`/api/health`).
5. Log retention and alerts for 5xx errors.
---
## 12. Frontend hook-up
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** users: when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**, the create layout may **hydrate** in-memory flow state from **`GET /api/drafts/me`** once per session ([`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx)), including conflict handling if anonymous storage also has data. Without sync, signed-in progress stays **in memory** until **Save & Exit** (no automatic server read on entry). **Canonical wizard step order, URLs, and Figma product stages** (**Create Community** → **Create Custom CommunityRule****Review and complete**) are documented in [`docs/create-flow.md`](create-flow.md). The route **`/create/review-template/[slug]`** is an **auxiliary** template preview (not a numbered wizard step); a **full create-from-template** path will likely be **separate route(s)** when defined. **Prefilling the wizard or landing on `final-review` from a template** is **not** shipped yet — see **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** / Ticket 17 in [docs/backend-linear-tickets.md](backend-linear-tickets.md).
**Step 2.** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` to enable **PUT** on **Save & Exit** and after **magic-link transfer** from the save-progress exit modal.
**Step 3.** Sign-in: **Log in** in the header opens the **modal** ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** is still used for verify **error** redirects and bookmarks. Flow: request magic link → open verify URL → session cookie → `GET /api/auth/session` / `/api/drafts/me` as needed.
**Step 4.** On publish, call `POST /api/rules` from the completed step when the backend is required (wire when the final review UI is ready).
**Step 5.** **Profile / dashboard** (`/profile` or agreed path): signed-in hub for **my rules** (after Ticket 15 APIs exist), **duplicate** / **delete** rule actions, **logout**, **delete account**—aligned with [Figma profile](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069). **Change email** in design is **deferred** (hide, “coming soon,” or backlog) until a future account ticket; greeting copy can stay **static** or use **email local-part in UI only**—no `displayName` field required for MVP.
**Step 6.** **Templates:** **Tickets 78** — seed `RuleTemplate` and load **`GET /api/templates`** in home / create surfaces (flat list, optional `featured`). **Ticket 16 / [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** — add **facet-based recommendations** and **spreadsheet ingestion** when product is ready (matrix rows + dimension columns like the decision-making workbook).
---
## 13. Optional later
- **Template recommendation matrix** + `.xlsx` / Sheets import pipeline — see **Ticket 16** / **[CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)** (also §4 `RuleTemplate` note); not bundled into v1 template list work.
- **Session library** spike (Auth.js, Lucia) if custom lifecycle cost grows.
- **Redis** (or similar) for **shared magic-link rate limits** and horizontal scale.
- **RuleDraft** versioning or multiple drafts per user.
- Standalone **API service** (Fastify/Hono) if scaling or workers demand it.
- **OpenAPI** if external API clients appear.
- **Fourth environment** or stricter rate limiting at the edge.
---
## 14. Useful commands
| Command | When |
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
| `npx prisma studio` | Inspect/edit DB locally. |
| `npx prisma migrate dev` | After changing `schema.prisma` in development. |
| `npx prisma migrate deploy` | Apply migrations in staging/production. |
| `docker compose up -d postgres mailhog` | Local DB + mail UI (http://localhost:8025). |
| `docker build -t community-rule .` | Optional production image (Next **standalone** + `node server.js`; see repo `Dockerfile`). |
---
## External reading
- [Docker: development best practices](https://docs.docker.com/develop/dev-best-practices/)
- [Docker Compose in production](https://docs.docker.com/compose/how-tos/production/)
@@ -1,401 +0,0 @@
# Container/Presentation Pattern
## Overview
The Container/Presentation pattern separates component logic from presentation, improving testability, reusability, and maintainability. This pattern is now the standard for complex components in this codebase.
## Motivation
### Benefits
- **Testability**: Pure presentation components can be tested independently with simple prop assertions
- **Reusability**: Presentation components can be reused with different data sources or logic
- **Maintainability**: Clear separation makes it easier to locate and modify specific concerns
- **Performance**: Easier to optimize rendering with React.memo on pure components
### When to Use
Use this pattern for components that have:
- Business logic or state management
- Data fetching or API calls
- Analytics tracking
- Complex event handlers
- Custom hooks usage
- Dynamic imports or side effects
Simple presentational components (e.g., `Button`, `Avatar`) can remain as single files.
## Folder Structure
Each component following this pattern should have this structure:
```
app/components/[ComponentName]/
├── index.tsx # Exports container as default
├── [ComponentName].container.tsx # Logic, hooks, state management
├── [ComponentName].view.tsx # Pure presentation component
└── [ComponentName].types.ts # Shared TypeScript types
```
### File Responsibilities
#### `index.tsx`
- Exports the container component as the default export
- Optionally exports types for external use
- Maintains backward compatibility with existing import paths
```typescript
export { default } from "./AskOrganizer.container";
export type { AskOrganizerProps } from "./AskOrganizer.types";
```
#### `[ComponentName].container.tsx`
**Contains all logic:**
- React hooks (`useState`, `useEffect`, custom hooks)
- Event handlers and business logic
- Data fetching and API calls
- Analytics tracking
- State management
- Computed values and derived state
- Side effects
**Should NOT contain:**
- JSX layout details (beyond composing the view)
- Inline styles or complex className logic (pass as props)
- Direct DOM manipulation
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../../hooks";
import { AskOrganizerView } from "./AskOrganizer.view";
import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({
event: "contact_button_click",
category: "engagement",
component: "AskOrganizer",
});
// ... additional logic
};
// Compute derived props
const variantStyles = computeVariantStyles(props.variant);
return (
<AskOrganizerView
{...props}
onContactClick={handleContactClick}
variantStyles={variantStyles}
/>
);
}
export default memo(AskOrganizerContainer);
```
#### `[ComponentName].view.tsx`
**Pure presentation:**
- Receives all data via props
- Renders JSX based on props
- No hooks, no state, no side effects
- Only imports other presentational components
**Should NOT contain:**
- `useState`, `useEffect`, or any hooks
- Event handler implementations (receive as callbacks)
- Data fetching or API calls
- Analytics tracking
- Business logic
```typescript
import ContentLockup from "../ContentLockup";
import Button from "../Button";
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
export function AskOrganizerView({
title,
subtitle,
description,
buttonText,
buttonHref,
variant,
onContactClick,
variantStyles,
...props
}: AskOrganizerViewProps) {
return (
<section className={variantStyles.container}>
<ContentLockup
title={title}
subtitle={subtitle}
description={description}
/>
<Button
href={buttonHref}
onClick={onContactClick}
>
{buttonText}
</Button>
</section>
);
}
```
#### `[ComponentName].types.ts`
- Shared TypeScript interfaces and types
- Public props interface (used by consumers)
- Internal view props (used between container and view)
- Any utility types specific to the component
```typescript
export interface AskOrganizerProps {
title?: string;
subtitle?: string;
buttonText?: string;
buttonHref?: string;
variant?: "centered" | "left-aligned" | "compact" | "inverse";
onContactClick?: (data: ContactClickData) => void;
}
export interface AskOrganizerViewProps extends AskOrganizerProps {
onContactClick: () => void;
variantStyles: {
container: string;
buttonContainer: string;
};
}
```
## Rules of Thumb
### Container Components
**DO:**
- Use React hooks (`useState`, `useEffect`, custom hooks)
- Handle all event handlers and business logic
- Fetch data and manage loading states
- Track analytics events
- Compute derived values from props/state
- Compose the view component with computed props
**DON'T:**
- Include complex JSX layout (delegate to view)
- Mix presentation logic with business logic
- Access DOM directly (use refs when necessary)
### View Components
**DO:**
- Receive all data via props
- Render JSX based on props
- Import only presentational components
- Use simple conditional rendering
- Accept callback props for user interactions
**DON'T:**
- Use any React hooks
- Manage state or side effects
- Fetch data or make API calls
- Track analytics directly
- Implement business logic
- Access browser APIs directly
## Example: AskOrganizer
### Before (Monolithic)
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../hooks";
import ContentLockup from "./ContentLockup";
import Button from "./Button";
const AskOrganizer = memo(({ title, variant, ...props }) => {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" });
};
return (
<section>
<ContentLockup title={title} />
<Button onClick={handleContactClick}>Ask an organizer</Button>
</section>
);
});
```
### After (Container/Presentation)
**AskOrganizer.container.tsx:**
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../../hooks";
import { AskOrganizerView } from "./AskOrganizer.view";
import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" });
};
return <AskOrganizerView {...props} onContactClick={handleContactClick} />;
}
export default memo(AskOrganizerContainer);
```
**AskOrganizer.view.tsx:**
```typescript
import ContentLockup from "../ContentLockup";
import Button from "../Button";
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
export function AskOrganizerView({
title,
onContactClick,
...props
}: AskOrganizerViewProps) {
return (
<section>
<ContentLockup title={title} />
<Button onClick={onContactClick}>Ask an organizer</Button>
</section>
);
}
```
## Migration Checklist
When converting an existing component to this pattern:
- [ ] **Identify separation points**
- [ ] List all hooks and state management
- [ ] List all event handlers and business logic
- [ ] List all data fetching and side effects
- [ ] Identify pure presentation JSX
- [ ] **Create folder structure**
- [ ] Create `[ComponentName]/` folder
- [ ] Create `[ComponentName].types.ts` with shared types
- [ ] Create `[ComponentName].view.tsx` with pure presentation
- [ ] Create `[ComponentName].container.tsx` with all logic
- [ ] Create `index.tsx` exporting container
- [ ] **Extract types**
- [ ] Move component props interface to `types.ts`
- [ ] Create view props interface extending container props
- [ ] Export types from `index.tsx` for external use
- [ ] **Move presentation to view**
- [ ] Copy JSX to view component
- [ ] Remove all hooks and state
- [ ] Replace event handlers with callback props
- [ ] Replace computed values with props
- [ ] Ensure view is a pure function
- [ ] **Move logic to container**
- [ ] Move all hooks to container
- [ ] Move event handlers to container
- [ ] Move data fetching to container
- [ ] Compute derived props in container
- [ ] Render view component with computed props
- [ ] **Update exports**
- [ ] Export container as default from `index.tsx`
- [ ] Export types from `index.tsx`
- [ ] Delete original component file
- [ ] Verify import paths still work
- [ ] **Update tests**
- [ ] Verify tests still pass (imports should resolve automatically)
- [ ] Update any tests that relied on implementation details
- [ ] Add tests for view component with mocked props if needed
- [ ] **Update Storybook**
- [ ] Verify stories still work (imports should resolve automatically)
- [ ] Optionally add view-only stories with mocked props
## Refactored Components
The following components have been refactored to use this pattern:
-**AskOrganizer** - Analytics tracking and event handlers
-**NumberedCards** - Schema generation with `useSchemaData` hook
-**FeatureGrid** - Memoized feature data structures
-**WebVitalsDashboard** - Dynamic imports, data fetching, complex state
-**Select** - Complex form state management with refs and keyboard navigation
These components serve as reference implementations for the pattern.
## Remaining Components
The following components are candidates for future conversion:
### High Priority (Complex Logic)
- `Header` / `HomeHeader` - Navigation state, conditional rendering logic
- `MenuBar` - Menu state management, keyboard navigation
- `ContextMenu` - Positioning logic, click outside handling
- `RadioGroup` - Group state management
- `ToggleGroup` - Group state management
### Medium Priority (Some Logic)
- `ContentContainer` - Data fetching or transformation
- `RelatedArticles` - Data fetching, filtering logic
- `RuleStack` - Complex rendering logic
- `LogoWall` - Animation or interaction logic
### Low Priority (Mostly Presentational)
- `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components
- `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation
- `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components
## Best Practices
1. **Start with complex components** - Components with the most logic benefit most from separation
2. **Keep it simple** - Don't over-engineer simple presentational components
3. **Maintain backward compatibility** - Import paths should remain unchanged
4. **Test both layers** - Test container for logic, view for presentation
5. **Document the pattern** - Add comments explaining non-obvious prop flows
6. **Use TypeScript strictly** - Leverage types to enforce the separation
## Additional Resources
- [React Container/Presenter Pattern](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
- [Separation of Concerns in React](https://react.dev/learn/thinking-in-react)
---
**Last Updated**: April 2025
**Maintained by**: CommunityRule Development Team
+55 -356
View File
@@ -1,382 +1,81 @@
# i18n Translation Workflow Guide
# Translations & UI copy workflow
This guide explains how to work with translations in the CommunityRule application. The app uses a hybrid approach combining globalized, shared UI elements with context-aware, localized content pages, making it easy for content creators and contributors to update text without modifying component code.
This guide is for **content editors** updating user-visible text. The
implementation contract (file layout, `useMessages` access pattern, key
casing rules) lives in `.cursor/rules/localization.mdc`.
## Overview
## Where copy lives
All UI text is stored in JSON files under `messages/en/`. The structure follows best practices:
All UI text is JSON under `messages/en/`:
- **Page-specific content** lives in `pages/` (varies by page context)
- **Component defaults** live in `components/` (shared across pages)
- **Common strings** live in `common.json` (shared UI elements)
Components reference these translations using keys, allowing content to be edited independently of the codebase.
## Directory Structure
```
messages/
en/
pages/
home.json # Home page specific content
learn.json # Learn page specific content
components/
heroBanner.json # Component defaults (aria-labels, alt texts)
numberedCards.json # Component defaults
askOrganizer.json # Component defaults
featureGrid.json # Component defaults
footer.json # Shared across pages
header.json # Shared across pages
common.json # Shared UI strings (buttons, links, labels)
navigation.json # Navigation items
metadata.json # Page metadata (title, description)
index.ts # Exports all messages
```text
messages/en/
common.json # shared strings (buttons, links, generic labels)
navigation.json # site nav items
metadata.json # page <title> / description
pages/<slug>.json # one file per page (home, learn, …)
components/<name>.json # one file per shared component default
create/<step>.json # one file per create-flow step
index.ts # wires all bundles together
```
## When to Use `pages/` vs `components/`
The split is intentional:
### Use `pages/` for:
- **`pages/`** — copy that varies by page context (titles, hero subheads).
- **`components/`** — defaults that ride along with a component on every
page (aria-labels, alt text patterns).
- **`common.json`** — strings reused across many components (e.g. "Cancel",
"Learn more").
- **`create/`** — wizard step copy (mirrors the `screenId`).
- **Page-specific content**: Titles, subtitles, descriptions that vary by page
- **Context-aware text**: Content that changes based on where the component is used
- **User-facing content**: All text that users see on a specific page
## Editing existing copy
**Example:** The home page hero banner title "Collaborate" goes in `pages/home.json`, not `components/heroBanner.json`
1. Find the bundle: search `messages/en/` for the existing English string.
2. Edit the value. Leave the key alone.
3. Save and run `npm run dev` — text updates on reload.
### Use `components/` for:
If you can't find the string, it may still be hard-coded. Open an issue or
ping a developer; do not change the component file directly.
- **Component defaults**: Aria-labels, alt text patterns, shared behavior text
- **Shared across pages**: Text that doesn't vary by page context
- **Accessibility text**: Aria-labels and alt texts that are component-level
## Adding a new key
**Example:** The hero banner image alt text "Hero illustration" stays in `components/heroBanner.json` because it's the same across all pages
### Use `common.json` for:
- **Shared UI strings**: Buttons, links, labels used across multiple components
- **Global strings**: Text that appears in many places
## Page-Specific Translations
For page-specific content, use the `pages.*` namespace pattern:
**Server Components:**
```typescript
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function LearnPage() {
const t = (key: string) => getTranslation(messages, key);
const contentLockupData = {
title: t("pages.learn.contentLockup.title"),
subtitle: t("pages.learn.contentLockup.subtitle"),
};
return <ContentLockup {...contentLockupData} />;
}
```
**Client Components:**
```typescript
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
}
```
## Adding New Translation Keys
### 1. Identify the Component
Determine which component needs the translation. If it's a shared string (like a button label), add it to `common.json`. Otherwise, add it to the component-specific file.
### 2. Add the Key to the JSON File
Open the appropriate JSON file and add your translation key. Use descriptive, semantic keys:
**Good:**
1. Decide which bundle owns the copy (page vs component vs common).
2. Add a descriptive camelCase key. Group related copy in nested objects.
3. If you created a new JSON file, register it in `messages/en/index.ts`
(a developer will help if you're unsure).
4. Reference the key from the component using `useMessages()` (see the
localization rule for the snippet).
```json
{
"heroBanner": {
"title": "Collaborate",
"subtitle": "with clarity"
}
}
```
**Bad:**
```json
{
"text1": "Collaborate",
"text2": "with clarity"
}
```
### 3. Use Nested Objects for Organization
Group related translations together:
```json
{
"numberedCards": {
"title": "How CommunityRule works",
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
}
}
}
```
### 4. Update the Component or Page
**For Page Components (Server Components):**
```typescript
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function MyPage() {
const t = (key: string) => getTranslation(messages, key);
// Use page-specific keys
const data = {
title: t("pages.home.heroBanner.title"),
subtitle: t("pages.home.heroBanner.subtitle"),
};
return <HeroBanner {...data} />;
}
```
**For Client Components:**
```typescript
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
// For page-specific content
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
// For component defaults
const tDefault = useTranslation("heroBanner");
return <img alt={tDefault("imageAlt")} />;
}
```
## Translation Key Naming Conventions
1. **Use camelCase** for keys: `buttonText`, `ariaLabel`
2. **Use descriptive names**: `createCommunityRule` not `btn1`
3. **Group by component**: Each component has its own namespace
4. **Use nested objects** for related strings: `buttons.createCommunityRule`
5. **Include context in comments**: Use `_comment` fields for clarity
Example:
```json
{
"_comment": "HeroBanner component translations",
"title": "Collaborate",
"subtitle": "with clarity",
"description": "Help your community make important decisions..."
}
```
## Extracting Strings from Components
When migrating a component to use translations:
1. **Identify hardcoded strings** in the component
2. **Create translation keys** in the appropriate JSON file
3. **Replace hardcoded strings** with `t("key.path")` calls
4. **Test the component** to ensure translations load correctly
### Example Migration
**Before:**
```typescript
export default function HeroBanner() {
return (
<div>
<h1>Collaborate</h1>
<p>with clarity</p>
</div>
);
}
```
**After:**
```typescript
"use client";
import { useTranslations } from "next-intl";
export default function HeroBanner() {
const t = useTranslations("heroBanner");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("subtitle")}</p>
</div>
);
}
```
## Adding a New Page
When creating a new page that needs translations:
1. **Create a page translation file**: `messages/en/pages/about.json` (for example)
2. **Add page-specific content**: All user-facing text for that page
3. **Import in index.ts**: Add the import and export in `messages/en/index.ts`
4. **Use in page component**: Use `t("pages.about.*")` pattern in your page
**Example:**
```typescript
// messages/en/pages/about.json
{
"_comment": "About page hero copy",
"hero": {
"title": "About Us",
"subtitle": "Learn more about our mission"
}
}
// app/about/page.tsx
const t = (key: string) => getTranslation(messages, key);
const title = t("pages.about.hero.title");
```
## Adding New Languages (Future)
When adding support for a new language:
1. **Create a new locale directory**: `messages/es/` (for Spanish, for example)
2. **Copy the English files** as a starting point (including `pages/` structure)
3. **Translate all strings** in the JSON files
4. **Test thoroughly** to ensure all translations are present
## Testing Translations
1. **Check for missing keys**: Ensure all translation keys used in components exist in the JSON files
2. **Verify type safety**: TypeScript will catch typos in translation keys at compile time
3. **Test in browser**: Run the dev server and verify text displays correctly
4. **Check for fallbacks**: Missing translations will show the key path (e.g., `heroBanner.title`)
## Best Practices
### For Content Creators
- **Edit JSON files directly**: No need to understand React or TypeScript
- **Use descriptive comments**: Add `_comment` fields to explain context
- **Maintain consistency**: Use the same terminology across components
- **Test changes**: Run the dev server to see your changes immediately
### For Developers
- **Use TypeScript**: Translation keys are type-safe
- **Namespace when possible**: Use `useTranslations("namespace")` for better organization
- **Server components first**: Prefer server-side translations for better performance
- **Extract incrementally**: Migrate components one at a time
## Common Patterns
### Buttons and CTAs
```json
{
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
"title": "About us",
"subtitle": "Why CommunityRule exists"
}
}
```
### Aria Labels
## Style notes
```json
{
"ariaLabels": {
"followBluesky": "Follow us on Bluesky",
"followGitlab": "Follow us on GitLab"
}
}
```
- **camelCase** for structural keys (`compactTitle`, `imageAlt`).
- **kebab-case** for content ids that match a URL slug, card id, or step
id (`"in-person-meetings"`, `"peer-mediation"`).
- Use `_comment` to leave context for the next editor.
- Keep terminology consistent — search the messages folder before coining a
new label.
### Dynamic Content
## Adding a new language (future)
For content that varies (like card text), use arrays or numbered keys:
```json
{
"cards": {
"card1": { "text": "First step" },
"card2": { "text": "Second step" },
"card3": { "text": "Third step" }
}
}
```
1. Create `messages/<locale>/` mirroring `messages/en/`.
2. Translate strings; keep keys identical.
3. Update `messages/en/index.ts` (or split it per locale) — a developer
will wire the locale switcher.
## Troubleshooting
### Translation Key Not Found
If you see a key path like `heroBanner.title` instead of the text:
1. Check the JSON file exists and has the key
2. Verify the key path matches exactly (case-sensitive)
3. Restart the dev server if you just added the key
### TypeScript Errors
If TypeScript complains about translation keys:
1. Ensure the key exists in the JSON file
2. Check for typos in the key path
3. Verify the namespace is correct if using `useTranslations("namespace")`
### Missing Translations
If text doesn't appear:
1. Check the browser console for errors
2. Verify the component is wrapped in `MessagesProvider` (for client components)
3. Ensure `getTranslation()` is called correctly in server components
4. Check if you're using the correct namespace (`pages.*` vs component defaults)
## Architecture: Hybrid Approach
This implementation follows the recognized best practice of combining:
- **Globalized, shared UI elements**: Component defaults in `components/` (aria-labels, alt texts)
- **Context-aware, localized content pages**: Page-specific content in `pages/` (titles, descriptions)
This allows:
- Components to remain flexible and reusable
- Page content to be easily edited without code changes
- Clear separation between shared defaults and page-specific content
- Scalable structure for adding new pages
## Resources
- Component defaults in `messages/en/components/`
- Page-specific content in `messages/en/pages/`
- Shared UI strings in `messages/en/common.json`
---
**Last Updated**: January 2025
**Maintained by**: CommunityRule Development Team
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Key path renders instead of text (e.g. `hero.title`) | Missing key or typo | Check spelling and bundle path |
| Copy doesn't update | Dev server cache | Restart `npm run dev` |
| TypeScript red squiggle | Bundle not registered | Add the import in `messages/en/index.ts` |
@@ -0,0 +1,728 @@
# Template Recommendation Matrix — Implementation Context (CR-88)
**Status:** Draft / context doc. Reference only — not yet implemented.
**Linear:** [CR-88](https://linear.app/community-rule/issue/CR-88/backend-template-recommendation-matrix-xlsx-sheets-ingestion)
**Roadmap:** [`docs/backend-roadmap.md`](backend-roadmap.md) §4 (`RuleTemplate`) and §13.
**Spec ticket:** [`docs/backend-linear-tickets.md`](backend-linear-tickets.md) Ticket 16.
This doc consolidates the **four product-authored matrix spreadsheets** with the
**existing data model, create-flow facets, and section structure** so we have a
single reference while implementing the importer + recommendation API.
> **Scope note:** No data, API, or UI surface for this feature is in production
> yet. **Backwards compatibility is not a constraint** — we will replace the
> hand-typed `prisma/seed.ts` `COMPOSITION_BY_SLUG` map, the existing
> `GET /api/templates` response shape, and the static `messages/en/create/*.json`
> card decks where it makes the design cleaner.
---
## 1. Goal (one paragraph)
Replace the hand-curated `prisma/seed.ts` `COMPOSITION_BY_SLUG` map with a
**spreadsheet-authored matrix** for each rule section
(Communication, Membership, Decision-making, Conflict management — and later
Values), where each row is a **method/pattern card** and each column is either
**long-form copy that populates the card UI** or a **facet flag** (✓/x or score)
that the recommendation engine uses to filter and rank cards based on the
user's create-flow answers (community size, organization type, location/scale,
maturity).
The same authoring contract should make it trivial for product to ship updated
spreadsheets and have the create-flow card decks (and the home/templates page
recommendations) update without any code changes.
---
## 2. The four spreadsheets
All four xlsx files share **the same column shape**: leading **content
columns** + trailing **facet columns** (✓ / x cells). Sheet name is `Current`
in every workbook.
### 2.1 Shared facet columns (last 19, identical across the four sheets)
Order is preserved here because the columns are positional in the sheets:
| # | Column header (xlsx) | Maps to wizard step / state field | Wizard chip label (`messages/en/create/...`) |
|---|---|---|---|
| 1 | `1 member` | `community-size``selectedCommunitySizeIds` (id `"1"`) | `1 member` |
| 2 | `2-5 members` | id `"2"` | `2-5 members` |
| 3 | `6-12 members` | id `"3"` | `6-12 members` |
| 4 | `13-100 members` | id `"4"` | `13-100 members` |
| 5 | `100-100,000 members` | id `"5"` | `100-100,000 members` |
| 6 | `Organization Type:DAO` (or `DAO` in conflict/comms/membership) | `community-structure``selectedOrganizationTypeIds` (id `"6"` in `organizationTypes`) | `DAO` |
| 7 | `Organization Type:For profit business` (or `For profit business`) | id `"5"` in `organizationTypes` | `For profit business` |
| 8 | `Organization Type:Nonprofit` (or `Nonprofit`) | id `"4"` in `organizationTypes` | `Nonprofit` |
| 9 | `Organization Type:Open source project` (or `Open source project`) | id `"3"` | `Open source project` |
| 10 | `Organization Type:Mutual aid` (or `Mutual aid`) | id `"2"` | `Mutual aid` |
| 11 | `Organization Type: Workers coop` (or `Workers coop`) | id `"1"` | `Workers coop` |
| 12 | `Location: Global` (or `Global`) | `community-structure``selectedScaleIds` (id `"4"` in `scaleOptions`) | `Global` |
| 13 | `Location: National` (or `National`) | id `"3"` | `National` |
| 14 | `Location: Regional` (or `Regional`) | id `"2"` | `Regional` |
| 15 | `Location: Local` (or `Local`) | id `"1"` | `Local` |
| 16 | `Organizational Maturity: Early stage` (or `Early stage`) | `community-structure``selectedMaturityIds` (id `"1"` in `maturityOptions`) | `Early stage` |
| 17 | `Organizational Maturity: Growth stage` (or `Growth stage`) | id `"2"` | `Growth stage` |
| 18 | `Organizational Maturity: Established` (or `Established`) | id `"3"` | `Established` |
| 19 | `Organizational Maturity: Enterprise` (or `Enterprise`) | id `"4"` | `Enterprise` |
**Important normalization rules (importer must enforce):**
- Decision-making prefixes columns with `Organization Type:`, `Location:`,
`Organizational Maturity:`. The other three sheets drop the prefix. Importer
should normalize to a single canonical key (e.g.
`orgType.workersCoop`, `scale.local`, `maturity.earlyStage`, `size.6_12`).
- Cell value semantics: `✓` → match, `x` (lowercase) → no match, blank → no
match, numbers → optional weighted score (only `Decision-making.xlsx` row 32
contains a non-symbol cell — `"Military, Corporations"` in the size column —
see §2.4 data-quality issues).
- Wizard chip ids are **positional 1..N** within each `messages/en/create/*`
array (see `chipRowsFromLabels` in
`app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` lines 4957).
The importer should emit a stable lookup table mapping
`(facetGroup, label) → wizardChipId` so the recommendation engine can match
a user's `selectedXxxIds` against the matrix without depending on label
spelling.
- Curly apostrophes appear in `Workers coop`. Compare on a normalized key,
not on raw label.
### 2.2 Communication Methods (`Communication Methods.xlsx`, sheet `Current`)
Maps 1:1 to `messages/en/create/communication.json` and the
`communication-methods` step
(`app/(app)/create/screens/card/CommunicationMethodsScreen.tsx`).
**Content columns (positions 15):**
| Sheet column | Card field |
|---|---|
| `Label` | `cards[<id>].label` and `modals[<id>].title` |
| `Description` | `cards[<id>].supportText` and `modals[<id>].description` |
| `Core Principle & Scope` | `modals[<id>].sections.corePrinciple` |
| `Logistics, Admin & Norms` | `modals[<id>].sections.logisticsAdmin` |
| `Code of Conduct` | `modals[<id>].sections.codeOfConduct` |
`SECTION_FIELDS = ["corePrinciple", "logisticsAdmin", "codeOfConduct"]` is
the source of truth (`CommunicationMethodsScreen.tsx`).
**Card rows (11):** In-Person Meetings · Signal · Video Meetings · Loomio ·
Matrix / Element · GitHub / GitLab · Discord · Email Distribution List · Slack
· WhatsApp · Discourse (Forum).
### 2.3 Membership / Group-Membership (`Group_Membership_Methods.xlsx`, sheet `Current`)
Maps to the `membership-methods` step
(`app/(app)/create/screens/card/MembershipMethodsScreen.tsx`) and
`messages/en/create/membership.json`.
**Content columns (positions 15):**
| Sheet column | Card field (proposed naming) |
|---|---|
| `Label` | `cards[<id>].label` / modal title |
| `Description` | `cards[<id>].supportText` / modal description |
| `Eligibility & Philosophy` | modal section A (`eligibilityPhilosophy`) |
| `Joining Process` | modal section B (`joiningProcess`) |
| `Expectations & Removal` | modal section C (`expectationsRemoval`) |
**Card rows (19):** Open Access · Orientation Required · Invitation Only ·
Contribution Based · Mentorship · Peer Sponsorship · Consensus or Vote-Based
Approval · Trial Period / Provisional Membership · Referral System with
Screening · Membership Agreement or Pledge · Weighted or Tiered Membership ·
Hybrid Approval Process · Skill-Based Contribution · Pay-to-Join · Application
& Review · Identity Verification · Collective Interviews · Skill-Based
Evaluation · Lottery / Sortition.
> The wizard's existing `membership.json` modal section keys do not yet match
> these. Since backwards compatibility is not a constraint, **rename the
> wizard's section keys to match the matrix** (`eligibilityPhilosophy` /
> `joiningProcess` / `expectationsRemoval`) when wiring this up — the existing
> copy is placeholder.
### 2.4 Decision-making (`Decision-making.xlsx`, sheet `Current`)
Maps to the `decision-approaches` step
(`app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
`messages/en/create/rightRail.json`.
**Content columns (positions 17):**
| Sheet column | Card field (proposed naming) |
|---|---|
| `Label` | card title |
| `Description` | card support text |
| `Core Principle` | modal section A (`corePrinciple`) |
| `Applicable Scope` | modal section B (`applicableScope`) — free-text examples, e.g. `"Daily Operations, Minor Expenditures"` |
| `Consensus Level` | numeric 0.01.0 stored under `scalars.consensusLevel` (e.g. `0.51`, `0.67`, `1.0`) — drives the **Consensus axis** in any future visual sort/filter |
| `Step-by-Step Instructions` | modal section C (`stepByStep`) |
| `Objections & Deadlocks` | modal section D (`objectionsDeadlocks`) |
**Card rows (32):** Lazy Consensus · Do-ocracy · Consensus Decision-Making ·
Rotational Leadership · Modified Consensus · Consensus Seeking with Delegates
· Sociocracy · Supermajority Rule · Ranked Choice Voting · Range Voting ·
Majority Rule · Approval Voting · Weighted Voting · Cumulative Voting ·
Quadratic Voting · Continuous Voting · Holacracy · Collaborative Platforms ·
Deliberative Polling · Investor-Filled Board Seats · Elected Board of
Directors · Advisory Committees · Delegated Decision-Making · Executive
Committees · First Past the Post · Lottery/Sortition · Proof of Work · Random
Choice · Algorithm-Driven Decisions · Autocratic Decision-Making ·
Hierarchical Decision-Making · Negotiated Decisions.
**Data-quality issues to handle in the importer (do not silently drop):**
- Row 32 (`Hierarchical Decision-Making`): the `Consensus Level` cell contains
`"Military, Corporations"` (the value clearly belongs to `Applicable Scope`,
which itself already contains `"Military, Corporations"`). Importer should
flag this as a validation error and require a fix in the source workbook
rather than try to repair it.
- Row 11 (`Range Voting`): the **last facet column** (`Maturity: Enterprise`)
is empty in the source — treat empty as `x` (no match) **only after** the
importer logs a warning so the author knows it wasn't intentional ✓.
### 2.5 Conflict Management (`Conflict Management Methods.xlsx`, sheet `Current`)
Maps to the `conflict-management` step
(`app/(app)/create/screens/card/ConflictManagementScreen.tsx`) and
`messages/en/create/conflictManagement.json`.
**Content columns (positions 16):**
| Sheet column | Card field (proposed naming) |
|---|---|
| `Title` | card title (note: not `Label` like the other three) |
| `Description` | card support text |
| `Core Principle` | modal section A (`corePrinciple`) |
| `Applicable Scope` | modal section B (`applicableScope`) |
| `Process Protocol` | modal section C (`processProtocol`) |
| `Restoration & Fallbacks` | modal section D (`restorationFallbacks`) |
**Card rows (19):** Peer Mediation · Conflict Resolution Council · Facilitated
Negotiation · Ad Hoc Arbitration · Conflict Workshops · Supermajority Vote ·
Interest-Based Bargaining · Restorative Practices · Mediation · Circle
Processes · Judicial Committees · Managerial Decision · Internal Tribunal ·
Consensus Building · Binding Arbitration · Non-Binding Arbitration · Binding
Contracts · Lottery/Sortition · Rotational Judging.
> Conflict Management sheet uses `Title` instead of `Label` and omits the
> `Organization Type:` / `Location:` / `Organizational Maturity:` prefixes —
> normalize both at import time.
---
## 3. Existing data model & wizard surface area
### 3.1 `RuleTemplate` (today)
```64:73:prisma/schema.prisma
model RuleTemplate {
id String @id @default(cuid())
slug String @unique
title String
category String?
description String?
body Json
sortOrder Int @default(0)
featured Boolean @default(false)
}
```
`body` JSON is the rendered rule document
(`{ sections: [{ categoryName, entries: [{ title, body }] }, ...] }`),
authored today by the `bodyFromXlsxComposition()` helper in
`prisma/seed.ts` from a hand-typed `COMPOSITION_BY_SLUG` map.
**Section ordering (canonical):** Values → Communication → Membership →
Decision-making → Conflict management. Final-review and `governancePatternBody`
both rely on this exact order and casing.
```16:60:prisma/seed.ts
function governancePatternBody(coreValues: string): Prisma.InputJsonValue {
return {
sections: [
{ categoryName: "Values", entries: [{ title: "Core stance", body: coreValues }] },
{ categoryName: "Communication", entries: [...] },
{ categoryName: "Membership", entries: [...] },
{ categoryName: "Decision-making", entries: [...] },
{ categoryName: "Conflict management", entries: [...] },
],
};
}
```
### 3.2 Wizard facets captured today (`CreateFlowState`)
```83:95:app/(app)/create/types.ts
selectedCommunitySizeIds?: string[];
selectedOrganizationTypeIds?: string[];
selectedScaleIds?: string[];
selectedMaturityIds?: string[];
selectedCoreValueIds?: string[];
selectedCommunicationMethodIds?: string[];
selectedMembershipMethodIds?: string[];
selectedDecisionApproachIds?: string[];
selectedConflictManagementIds?: string[];
```
The first four are exactly the four **facet groups** in the matrix sheets. The
last four are the user's chosen **cards per section**, which the recommendation
flow can either pre-select (when picked from a template) or feed back into
ranking.
These same fields are validated server-side by `createFlowStateSchema` in
`lib/server/validation/createFlowSchemas.ts` (lines 47106) — the recommend
endpoint should reuse that schema (or a strict subset) instead of redefining
the facet shape.
### 3.3 Wizard step order
Source of truth is `app/(app)/create/utils/flowSteps.ts` (`FLOW_STEP_ORDER`). The
relevant slice is:
```
review → core-values → communication-methods → membership-methods →
decision-approaches → conflict-management → confirm-stakeholders → final-review
```
`docs/create-flow.md`'s step table is **stale**; trust `flowSteps.ts`.
### 3.4 Where templates already surface in the UI
| Surface | File |
|---|---|
| Marketing home "Popular templates" | `app/(marketing)/_components/MarketingRuleStackSection.tsx` |
| Templates index | `app/(marketing)/templates/page.tsx` |
| Template preview (by slug) | `app/(app)/create/review-template/[slug]/page.tsx` |
| "Use without changes" → publish | `app/(app)/create/CreateFlowLayoutClient.tsx` `handleUseTemplateWithoutChanges` |
| API list | `app/api/templates/route.ts` (GET only, no params) |
There is currently **no** recommendation logic, no facet filtering, and the
`/create/informational?template=<slug>` query param is a known no-op (see
`CreateFlowLayoutClient.tsx` lines 479482).
---
## 4. Repo conventions to follow (don't reinvent)
These are the patterns the implementation must match. References point at the
canonical example for each.
### 4.1 API routes (`app/api/**/route.ts`)
`app/api/drafts/me/route.ts` is the reference — every new route in this
feature must match this exact shape:
1. `if (!isDatabaseConfigured()) return dbUnavailable();` — always first.
(`lib/server/env.ts`, `lib/server/responses.ts`).
2. For auth'd routes: `const user = await getSessionUser();` then
`return NextResponse.json({ error: "Unauthorized" }, { status: 401 });` if
missing. (Recommendation read endpoints can stay unauthenticated.)
3. For request bodies: `readLimitedJson(request)` →
`<schema>.safeParse(parsed.value)` → `jsonFromZodError(validated.error)` on
failure. (`lib/server/validation/requestBody.ts`,
`lib/server/validation/zodHttp.ts`).
4. Success: `NextResponse.json({ <key>: data })` — flat object with one or two
named keys, no `success: true` envelope.
5. Errors: structured `{ error: { code, message } }` (Zod path) or simple
`{ error: "..." }` (auth path). Match what's already in the repo.
6. Server-side query helpers swallow Prisma failures and return `[]`/`null`
(see `listRuleTemplatesFromDb` in `lib/server/ruleTemplates.ts` lines 930).
Routes do **not** wrap helper calls in `try/catch`.
### 4.2 Zod schemas live in `lib/server/validation/`
- One file per feature area (e.g. `createFlowSchemas.ts`, future
`templateRecommendationSchemas.ts`).
- Export the schema **and** the inferred type
(`export type X = z.infer<typeof xSchema>`).
- Wrap any free-form JSON blobs with `assertPlainJsonValue` /
`DEFAULT_PLAIN_JSON_LIMITS` (`lib/server/validation/plainJson.ts`) so the
size/depth bounds match the rest of the API.
- Reuse `FLOW_STEP_ORDER` and existing array bounds where they overlap (see
the `selectedXxxMethodIds` arrays in `createFlowStateSchema`).
### 4.3 Prisma access
- Singleton: `import { prisma } from "lib/server/db";` — never
`new PrismaClient()` from app code. (Standalone scripts under `scripts/` /
`prisma/` may instantiate their own, matching `prisma/seed.ts` lines
363403.)
- Server-only "fetch/list" helpers live under `lib/server/<feature>.ts`,
return DTOs (not raw Prisma rows), and degrade gracefully
(`isDatabaseConfigured()` short-circuit, `try/catch` → empty result).
- No `$transaction` patterns exist yet; **introduce one** for the importer
(write `TemplateMethod` + `TemplateMethodFacet` rows atomically).
### 4.4 DTO style
- Hand-written `type` aliases that mirror a Prisma `select` clause, co-located
with the consumer (see `RuleTemplateDto` in
`lib/create/fetchTemplates.ts` lines 514).
- For a feature with both client and server consumers, put the type in
`lib/<feature>/types.ts` and import from both sides.
### 4.5 Standalone scripts
- Use `tsx` (already a dev dep; entry point `package.json` `prisma.seed`
field).
- Layout matches `prisma/seed.ts`: `async function main()`, log a one-line
success summary, `console.error(e); process.exit(1)` on failure,
`await prisma.$disconnect()` in `finally`.
- Add an entry to `package.json` `scripts` (e.g.
`"templates:import": "tsx scripts/import-templates-xlsx.ts"`).
- No shared dotenv loader — rely on env from the shell / Next runtime.
- Support a `--dry-run` flag that validates + diffs without writing.
### 4.6 Tests
- Vitest under `tests/unit/*.test.ts` for parsers / validators / pure
functions (see `tests/unit/createFlowValidation.test.ts`).
- API routes are not unit-tested today; cover route behavior indirectly with a
`tests/unit/templateRecommendationSchemas.test.ts` (Zod) plus a fixture
workbook + importer test under `tests/unit/importTemplatesXlsx.test.ts`.
- E2E for the wizard (if needed) goes under `tests/e2e/*.spec.ts` — not
required for CR-88 acceptance.
- Test utilities: `tests/utils/test-utils.tsx` (`renderWithProviders`); MSW
server in `tests/msw/server.ts`. No Prisma mock helper exists; importer test
should use a fixture workbook and stub the `prisma` client at the import
site.
### 4.7 Logging
- Use `logger` from `lib/logger.ts` for any server-side info/warn/error in
scripts and route helpers (matches `app/api/auth/magic-link/request/route.ts`
lines 1415, 3545). No `apiError` helper exists; do not introduce one.
### 4.8 New deps
- `xlsx` (SheetJS) is **not** currently in `package.json`. Add it as a
**prod** dep only if the importer is invoked from app code; if the importer
is script-only, `devDependency` is fine. CR-88's plan calls for a
build/CLI-time importer, so `devDependencies` is the right home.
### 4.9 i18n / `messages/` constraint
- Card decks and modal copy are currently keyed in
`messages/en/<feature>.json` and read via
`useMessages().create.<feature>` (`app/contexts/MessagesContext.tsx`,
`messages/en/index.ts`).
- Only `en` is wired today, so we **don't** have a translation backlog
blocking us. The wiring step (§7) replaces `messages/en/create/{communication,
membership,rightRail,conflictManagement}.json` card/modal payloads with
values served by `GET /api/template-methods` (still keyed by the same
message namespace shape so future i18n can layer on if needed). Header
strings, button labels, and other purely-static UI copy stay in
`messages/en/*`.
### 4.10 `.cursorrules` scope
- The repo's `.cursorrules` PascalCase / lowercase normalization rule applies
to **React component props only**. It does **not** apply to API query
params, request bodies, or DB columns. The recommendation API uses lowercase
facet keys throughout (`orgType`, `scale`, `maturity`, `size`).
---
## 5. Authoring contract (informs §6 storage + §7 importer)
The four spreadsheets together imply this row schema (per matrix workbook):
```ts
type MatrixRow = {
/** Stable slug derived from `Label`/`Title` (kebab-case, lowercase, ascii).
* Used as the card id everywhere downstream. */
id: string;
/** Section this row belongs to. One of: communication, membership,
* decisionMaking, conflictManagement. (values is not yet sheet-driven.) */
section: "communication" | "membership" | "decisionMaking" | "conflictManagement";
/** Card-facing copy. Keys differ per section; importer normalizes. */
card: {
label: string;
description: string;
/** Section-specific long-form fields (34 per section). */
modalSections: Record<string, string>;
};
/** Optional numeric scalar fields (e.g. decisionMaking `Consensus Level`). */
scalars?: Record<string, number>;
/** Facet matches (✓ → true, x/blank → false). Keys are canonical facet ids. */
facets: {
size: Record<"1" | "2_5" | "6_12" | "13_100" | "100_100k", boolean>;
orgType: Record<"dao" | "forProfit" | "nonprofit" | "openSource" | "mutualAid" | "workersCoop", boolean>;
scale: Record<"global" | "national" | "regional" | "local", boolean>;
maturity: Record<"earlyStage" | "growthStage" | "established" | "enterprise", boolean>;
};
};
```
A sibling **manifest** documents the per-section section-key mapping and
column header → canonical facet/scalar key mapping, so the importer can be
stable across header rewording.
---
## 6. Storage (decided: normalized tables)
We are introducing two new Prisma models. Hand-typed `COMPOSITION_BY_SLUG` in
`prisma/seed.ts` is replaced by template rows that **reference** method slugs.
```prisma
model TemplateMethod {
id String @id @default(cuid())
section String // communication | membership | decisionMaking | conflictManagement
slug String
label String
description String
modalSections Json // { corePrinciple: "...", logisticsAdmin: "...", ... }
scalars Json? // { consensusLevel: 0.51 }
sortOrder Int @default(0)
facets TemplateMethodFacet[]
@@unique([section, slug])
@@index([section])
}
model TemplateMethodFacet {
id String @id @default(cuid())
methodId String
group String // size | orgType | scale | maturity
value String // e.g. "workersCoop"
matches Boolean // ✓ → true, x/blank → false
weight Float? // optional numeric override for future scoring
method TemplateMethod @relation(fields: [methodId], references: [id], onDelete: Cascade)
@@unique([methodId, group, value])
@@index([group, value, matches])
}
```
`RuleTemplate.body` continues to express a **chosen composition** of methods
(one or more per section). Curated templates in `prisma/seed.ts` become
references to `TemplateMethod.slug` instead of literal copy strings — when
copy changes in the spreadsheet, every template that references that slug
inherits the new copy.
A follow-up (out of scope for CR-88) may add a `RuleTemplateMethodLink` join
table if templates need ordering or per-template overrides; the current `body`
JSON shape is sufficient for the first ship.
---
## 7. Importer (`scripts/import-templates-xlsx.ts`)
Phased plan that the implementation agent can follow top-to-bottom. Mirrors
the structure of `prisma/seed.ts` (singleton client, `main()` +
`finally { $disconnect }`, `process.exit(1)` on failure).
1. **Read `.xlsx`** with [`xlsx`](https://www.npmjs.com/package/xlsx) (SheetJS,
add as devDependency) from a configurable input dir (default
`data/template-matrix/`). The four workbooks live there as committed
artifacts, not in `Downloads/`.
2. **Schema-validate per section** with Zod schemas that live in
`lib/server/validation/templateRecommendationSchemas.ts` so the API and
importer share the row shape: required column headers, allowed cell
symbols (``, `x`, blank, decimal for `Consensus Level`).
3. **Normalize**: kebab-case slug from label, strip
`Organization Type:` / `Location:` / `Organizational Maturity:` prefixes,
collapse whitespace, normalize curly quotes.
4. **Cross-sheet validation**: facet columns must match the canonical 19-column
set; unknown columns fail loudly via the importer (use `logger.error`).
5. **Diff & upsert** inside `prisma.$transaction([...])`: upsert
`TemplateMethod` rows by `(section, slug)`; delete + recreate
`TemplateMethodFacet` rows for each method.
6. **Emit a JSON snapshot** to `prisma/data/template-matrix.json` so
`prisma/seed.ts` can replay imports when the source workbooks aren't
available (e.g. CI seed without the spreadsheet checked in).
7. **Flags**: `--dry-run` (validate + diff, no writes), `--allow-warnings`
(don't fail on the row-32 / row-11 issues in §2.4 while authors are
iterating).
8. **Tests** in `tests/unit/importTemplatesXlsx.test.ts`: a fixture workbook
with two rows per section asserts both validation errors (unknown column,
bad symbol, miscategorized cell) and successful normalization. Reuse
Vitest patterns from `tests/unit/createFlowValidation.test.ts`.
Per Ticket 16 and the roadmap, **prefer batch `.xlsx` import** over a live
Google Sheets API in production. Authors export to `.xlsx` and a maintainer
runs `npm run templates:import` (or CI does on a `data/template-matrix/` change).
---
## 8. APIs
Two read endpoints. Both follow §4.1 conventions exactly: `dbUnavailable()`
guard → server helper from `lib/server/templateMethods.ts` →
`NextResponse.json({ ... })`.
### 8.1 `GET /api/templates` (rewrite)
Query params (all optional):
- `facet.size=<chipId>` (repeatable)
- `facet.orgType=<chipId>` (repeatable)
- `facet.scale=<chipId>` (repeatable)
- `facet.maturity=<chipId>` (repeatable)
Behavior:
- No params → existing curated ordering (`featured`, `sortOrder`, `title`),
no scoring.
- With facets → score each template by counting matching facets across the
methods referenced in its `body`; return ranked `templates` plus an
optional `scores` map.
Response:
```ts
{
templates: RuleTemplateDto[],
scores?: Record<string, { score: number; matchedFacets: string[] }>
}
```
Param parsing helper lives next to `listRuleTemplatesFromDb` in
`lib/server/ruleTemplates.ts` (e.g. `parseTemplateFacetsFromSearchParams`).
### 8.2 `GET /api/template-methods?section=<section>[&facet.*=...]`
Powers the four card-deck wizard steps and the section-level recommendation
view. Response:
```ts
{
section: "communication" | "membership" | "decisionMaking" | "conflictManagement",
methods: Array<{
slug: string;
label: string;
description: string;
modalSections: Record<string, string>;
scalars?: Record<string, number>;
/** Per-method facet match against the requested facets (omitted when no facets passed). */
matches?: { score: number; matchedFacets: string[] };
}>
}
```
Server helper: `listTemplateMethodsFromDb({ section, facets })` in
`lib/server/templateMethods.ts`. Same swallow-and-return-`[]` failure mode as
`listRuleTemplatesFromDb`.
### 8.3 `POST /api/templates/recommend` (follow-up, optional)
If product wants to send the full `CreateFlowState` (not just facet ids), the
body schema **reuses** `createFlowStateSchema` from
`lib/server/validation/createFlowSchemas.ts`. Same scoring engine, just a
richer input. Skip until §8.1 + §8.2 ship.
**Empty / partial facets:** never error. Fall back to today's ordering and
return all rows.
---
## 9. Wizard wiring (UI follow-on, not strictly part of CR-88)
Once the API exists:
- `communication-methods` / `membership-methods` / `decision-approaches` /
`conflict-management` screens each call
`GET /api/template-methods?section=...&facet.*=...`. The card label and
modal copy come from the API response, not from
`messages/en/create/<section>.json`. Static JSON in those four files is
pruned to the page-level strings (header titles, button labels, modal
chrome) only.
- Selecting a template on the marketing home or `templates/` page can prefill
the create flow's `selected*MethodIds` from the template's composition (this
closes the `?template=` no-op gap noted in
`CreateFlowLayoutClient.tsx`).
- Recommendations should never **hide** options from the user — ranking only.
Authors expect to see "all 32 decision-making patterns" with the ✓-matching
ones surfaced first.
---
## 10. Open questions for product before coding
1. **Should `Values` also be sheet-driven?** Today it's free-text only and
not in any of the four matrices. Roadmap implies eventual parity.
2. **Scoring vs filtering**: do we want to **hide** non-✓ rows when a facet
is set, or only **rank** them? Recommend ranking with a soft cutoff.
3. **Per-template featured composition vs library-wide**: should
`RuleTemplate` rows continue to exist as named compositions
("Consensus", "Elected Board", etc.), or become derived from a
"this is the best mix for nonprofit + 13100 + early stage" scoring? Doc
today assumes the former — templates remain curated.
4. **Authoring source of truth**: are the `Downloads/*.xlsx` files committed
to `data/template-matrix/` going forward, or do they live in a Drive folder
pulled by the importer at build time? Recommend committing.
5. **Data validation strictness**: the current Decision-making sheet has a
miscategorized cell (row 32, see §2.4). Importer should fail by default,
with a `--allow-warnings` flag for in-progress edits.
---
## 11. Test plan (acceptance for CR-88)
- [ ] `scripts/import-templates-xlsx.ts` runs end-to-end on the four committed
workbooks with no errors and produces the expected DB diff (or JSON
snapshot).
- [ ] Editing a row in the source workbook and re-running the importer changes
the rank order returned by `GET /api/templates?facet.orgType=4`
(the `Nonprofit` chip id) without any manual Studio edit.
- [ ] `tests/unit/importTemplatesXlsx.test.ts` rejects each documented
validation failure (unknown column, bad symbol, miscategorized row).
- [ ] `tests/unit/templateRecommendationSchemas.test.ts` exercises the Zod
schemas the importer and API share.
- [ ] Manual smoke on the four wizard card-deck steps: facet-narrowed
ordering surfaces matching cards first; facetless GET returns the
full curated list.
- [ ] No regression in existing template surfaces (marketing home, templates
index, review-template preview).
---
## 12. Source files referenced
- `prisma/schema.prisma` — `RuleTemplate` model (lines 6473).
- `prisma/seed.ts` — current curated composition + xlsx-shaped helpers
(lines 1404).
- `app/api/templates/route.ts` — existing GET endpoint (to be rewritten).
- `app/api/drafts/me/route.ts` — reference route shape (`dbUnavailable` →
`getSessionUser` → `readLimitedJson` → `safeParse` → `jsonFromZodError`).
- `lib/server/db.ts` — Prisma singleton (lines 118).
- `lib/server/responses.ts` — `dbUnavailable()` (lines 18).
- `lib/server/ruleTemplates.ts` — `listRuleTemplatesFromDb` (lines 930).
- `lib/server/validation/createFlowSchemas.ts` — schema to reuse for
`POST /api/templates/recommend` (lines 47106).
- `lib/server/validation/requestBody.ts` — `readLimitedJson` (lines 1348).
- `lib/server/validation/zodHttp.ts` — `jsonFromZodError` (lines 417).
- `lib/server/validation/plainJson.ts` — `assertPlainJsonValue` /
`DEFAULT_PLAIN_JSON_LIMITS`.
- `lib/logger.ts` — server-side `logger`.
- `app/(app)/create/types.ts` — `CreateFlowState` and facet fields.
- `app/(app)/create/utils/flowSteps.ts` — canonical step order.
- `app/(app)/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
- `app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
derivation pattern (positional `String(i+1)`).
- `app/(app)/create/screens/card/CommunicationMethodsScreen.tsx` — section-field
contract (`SECTION_FIELDS`).
- `messages/en/create/{communitySize,communityStructure,communication,membership,rightRail,conflictManagement}.json` —
current static card / chip copy that the matrix supersedes.
- `lib/templates/governanceTemplateCatalog.ts`,
`lib/templates/templateGridPresentation.ts`,
`lib/create/fetchTemplates.ts` — current presentation/DTO layer.
- `tests/unit/createFlowValidation.test.ts` — Vitest pattern for new
schema/importer tests.
- Roadmap: `docs/backend-roadmap.md` §4 (lines 8385), §13.
- Spec: `docs/backend-linear-tickets.md` Ticket 16 (lines 280304).
## 13. Source workbooks
| File | Sheet | Rows | Cols | Section |
|---|---|---|---|---|
| `Communication Methods.xlsx` | `Current` | 11 cards | 24 | `communication` |
| `Group_Membership_Methods.xlsx` | `Current` | 19 cards | 24 | `membership` |
| `Decision-making.xlsx` | `Current` | 32 cards | 26 | `decisionMaking` |
| `Conflict Management Methods.xlsx` | `Current` | 19 cards | 25 | `conflictManagement` |
Counts include the header row. Decision-making has 26 columns because of two
extra content fields (`Consensus Level`, `Step-by-Step Instructions` vs the
4-section pattern of the others).