Files
community-rule/docs/guides/backend-linear-tickets.md
2026-05-23 16:52:18 -06:00

92 KiB
Raw Permalink Blame History

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), migrations, lib/server/*, Route Handlers under app/api/*, docker-compose.yml, Dockerfile, CONTRIBUTING.md, .env.example, 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 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 — Done), CR-85 (session lifecycle — Done)—see Linear table at the end of this doc. Change account email is Ticket 20 / CR-103 (split from Ticket 15 scope).

Audit note (Linear CR-72+ vs repo, 2026-04)

  • Done in Linear and shipped: CR-72CR-76, CR-77 (publish from create flow), CR-78 (template seed), CR-79, CR-88, CR-89. The CR-72 → CR-83 numbering is the original sequential plan, not current blocking order; the core product vertical through publish + templates is effectively complete in-repo.
  • Backlog (still open): CR-80 (web vitals — file-based route remains), CR-86 (profile + account + draft resume — UI mostly placeholder), CR-103 (change account email — Ticket 20), CR-90 / CR-91, CR-93 (template grid facets on marketing). CR-81 Done — public rule detail shipped: app/(marketing)/rules/[id]/page.tsx, app/api/rules/[id]/route.ts. CR-82 (migrate smoke): local npm run migrate:smoke + CONTRIBUTING.md / docs/testing-guide.md — in-repo Gitea workflow YAML removed; optional future remote job if hosted runners return. CR-84 Done — canonical error contract { error: { code, message }, details? } and x-request-id propagation shipped via lib/server/{responses,requestId,apiRoute}.ts; auth + drafts + rules routes migrated, remaining app/api/* are a follow-up pass. CR-85 Done — multi-device session policy + lazy expired-row cleanup (per-user prune on every sign-in plus ~5% global sweep, no cron); ADR comment block in lib/server/session.ts.
  • CR-83 Done (admin handoff + cutover plan): docs/guides/ops-backend-deploy.md shipped. Cloudron admin access on my.medlab.host granted; doc now covers (a) what's in place, (b) the side-by-side → apex cutover plan, and (c) the two open product questions + registry decision still outstanding. Steady-state operator runbook is split out into a follow-up — see Ticket 12 / CR-83 follow-ups below. Key new finding: legacy communityrule.info is a single Cloudron LAMP app (lamp.cloudronapp.php74@5.1.2) hosting marketing site + Express/MySQL backend + a broken Flask chatbot all in one container; all three retire together via CR-99 + CR-101.
  • CR-86 is no longer blocked by publish — CR-77 is Done; profile work is gated by implementation, not waiting on publish wiring.
  • Not in this ticket list but called out in docs/backend-roadmap.md: shared rate-limit store (e.g. Redis) before multi-instance; GET /api/create-flow/methods exists for facet scoring (Ticket 16 / CR-88) but is not duplicated as a separate doc ticket.

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 CLOUDRON_MAIL_SMTP_* 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 CLOUDRON_POSTGRESQL_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 CLOUDRON_MAIL_SMTP_* + 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—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 No Migrate smoke is local (npm run migrate:smoke); no server for CI.
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, plus note for /api/web-vitals.

Acceptance criteria:

  • A new contributor reading only the roadmap does not think the backend is unbuilt.
  • §13 Optional later (old §9) unchanged in intent — optional Redis, session-library spike, draft versioning, standalone API, OpenAPI, fourth env.

Status: CR-72 Done.

Files: 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 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) schemas:
    • createFlowStateSchema for draft payload.
    • publishedRuleDocumentSchema for document on POST /api/rules.
  3. In app/api/drafts/me/route.ts and 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:

  • TypeScript reflects the real shape of CreateFlowState (no unnecessary unknown for known keys).
  • Invalid draft/publish requests return 400, not 500.
  • Unit tests for schemas (Vitest) or route tests with MSW.

Status: CR-73 Done.

Files: app/(app)/create/types.ts, app/api/drafts/me/route.ts, app/api/rules/route.ts, lib/server/validation/ (Zod + plain-JSON checks), 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.


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.

Implementation (shipped):

  1. /login route and header modal — primary Log in entry is AuthModalProvider + app/components/modals/Login/; 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).
  5. Dev: without CLOUDRON_MAIL_SMTP_*, verify URL is logged; with Mailhog, use docker-compose.yml and CLOUDRON_MAIL_SMTP_SERVER=localhost + CLOUDRON_MAIL_SMTP_PORT=1025.
  6. Marketing header: When signed in (fetchAuthSession), Log in becomes Profile linking to /profile (placeholder until Ticket 15 / CR-86). Implemented in TopWithPathname.tsx + Top.container.tsx.

Acceptance criteria:

  • Happy path: user completes magic-link verify and GET /api/auth/session returns user in the same browser session.
  • Keyboard + screen-reader friendly forms (labels, errors associated with fields).
  • No secrets in client bundle.
  • Header shows Profile → placeholder /profile when session present; Log in when anonymous (opens modal, not only /login).

Status: CR-74 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)/profile/ (placeholder), app/components/modals/Login/, messages/en/pages/login.json, messages/en/pages/profile.json, messages/en/components/header.json, app/components/navigation/Top/Top.container.tsx, app/components/navigation/Top/TopWithPathname.tsx, lib/create/api.ts, app/api/auth/magic-link/request/route.ts, app/api/auth/magic-link/verify/route.ts, prisma/schema.prisma (MagicLinkToken), lib/server/mail.ts. Onboarding: CONTRIBUTING.md, .env.example.

Residual / before CR-75 (create-flow session UI)

Intent: Ticket 4 (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 / onboardingDone: CONTRIBUTING.md API table and sign-in section describe magic-link request/verify, dev log URL, and Mailhog. .env.example comments match.
  2. Smoke checklistDone: Email magic link (sign-in) in CONTRIBUTING.md; build-order §9 in docs/backend-roadmap.md includes the same happy path + session check.
  3. Staging / production URLsVerify 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 alignmentDone: docs/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 PUTs to /api/drafts/me when sync is on. Signed-in: Save & Exit PUTs via useCreateFlowExit when NEXT_PUBLIC_ENABLE_BACKEND_SYNC. Sign out for QA lives on ProfilePageClient. Site Log in opens the same modal overlay (AuthModalProvider), 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. Save-progress exit modal: Figma 22398:23743.

Implementation (repo):

  1. app/(app)/create/layout.tsx: session + enableAnonymousPersistence; anonymous exit → openLogin({ variant: 'saveProgress', nextPath }); signed-in exit → useCreateFlowExit.
  2. CreateFlowTopNav: i18n messages/en/create/topNav.json; logo + Share/Export/Edit (completed) + Exit/Save & Exit only.
  3. useCreateFlowExit: saveDraftToServer when sync + signed in; clearState + home.
  4. CreateFlowContext: optional anonymous localStorage mirror via enableAnonymousPersistence.
  5. QA: ProfilePageClient Sign out when session present.

Acceptance criteria:

  • Completed step still works; Save & Exit gating uses session + step (not conflated with completed only).
  • 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/hooks/useCreateFlowExit.ts, app/components/navigation/CreateFlowTopNav/, app/(app)/create/context/CreateFlowContext.tsx, messages/en/create/topNav.json, 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 + messages/en/create/draftHydration.json; skips ?syncDraft=1 / transfer-pending (PostLogin owns that). Wired in layout.
  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. Newer-updatedAt client compare remains optional.
  3. Save failures (API surface): Done (CR-76): saveDraftToServer returns SaveDraftResult with parsed API message; wired in useCreateFlowExit and PostLoginDraftTransfer.
  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 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:

  • 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).
  • 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, app/(app)/create/hooks/useCreateFlowExit.ts, app/(app)/create/PostLoginDraftTransfer.tsx, app/(app)/create/SignedInDraftHydration.tsx, app/(app)/create/layout.tsx, CreateFlowContext, 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.

Context: lib/create/api.ts wraps POST /api/rules with Zod-validated body (Ticket 2). Finalize flows through useCreateFlowFinalize from CreateFlowLayoutClient (final-reviewpublishRule/create/completed).

Implementation (shipped):

  1. Map CreateFlowStatetitle / summary / document via buildPublishPayload (and related builders).
  2. Call publishRule on explicit Finalize from final-review (useCreateFlowFinalize).
  3. 401openLogin with return path (Ticket 3 / AuthModalProvider).
  4. Success: navigate to completed with rule id in query string.

Acceptance criteria:

  • Published row appears in Postgres (PublishedRule) and GET /api/rules lists it.
  • User sees clear success/failure (banner / flow state; see finalize hook).

Files: app/(app)/create/hooks/useCreateFlowFinalize.ts, CreateFlowLayoutClient.tsx, app/api/rules/route.ts, lib/create/api.ts, lib/create/buildPublishPayload.ts.

Status: CR-77 Done.


Ticket 7 — Seed RuleTemplate data and document how to re-run

Depends on: none (API exists at app/api/templates/route.ts).

Goal: Curated templates exist in DB for recommendations (v1 = static curated list, no ML).

Not in v1 (this ticket): Facet-based method/template intelligence beyond a flat curated list — that work is Ticket 16 / CR-88 (facet data + APIs + wizard method ranking). Template marketing grids ranked by user facets are CR-93 (product/UI follow-up).

Implementation:

  1. Add Prisma seed: prisma/seed.ts with upsert on slug for idempotent runs.
  2. In 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 or home cards) — title, category, description, body JSON, sortOrder, featured.
  4. Document: npx prisma db seed in 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, CONTRIBUTING.md.

Status: CR-78 Done.


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 renders cards from API-shaped grid entries. The custom wizard uses app/(app)/create/[screenId]/page.tsx and docs/create-flow.md (Ticket 17).

Implementation (shipped):

  1. Server-first paint: MarketingRuleStackSection loads rows via listRuleTemplatesFromDb and passes initialGridEntries into RuleStack.container.tsx (avoids client fetch on LCP-critical home).
  2. Client fetch: lib/create/fetchTemplates.ts when initialGridEntries is not provided (GET /api/templates, credentials, abort); catalog fallback on error/empty.
  3. Templates index: app/(marketing)/templates/page.tsx server-loads the full grid for first paint.
  4. Empty / error: Fallback presentation via templateGridPresentation.ts + catalog slugs.

Acceptance criteria:

  • Changing a template row in the DB reflects after refresh (SSR path) or client refetch where the client path is used.
  • No layout shift regression on LCP-critical pages: dynamic RuleStack loading shell + server-provided initialGridEntries on home.

Files: app/components/sections/RuleStack/, app/(marketing)/_components/MarketingRuleStackSection.tsx, app/(marketing)/templates/, lib/create/fetchTemplates.ts, lib/templates/templateGridPresentation.ts.

Follow-up: CR-93 — rank marketing template cards by community facets (API already supports facet.*; UI wiring is product work).

Status: CR-79 Done.


Ticket 16 — Template recommendation matrix (facet data + APIs; no xlsx import)

Depends on: Tickets 78 (templates exist in DB and UI can fetch them).

Goal: Facet-aware recommendations for methods in the create-flow card decks, and API-level ranking for templates when facet.* query params are present — without a runtime .xlsx / SheetJS import (authoring stays committed JSON + Prisma seed, with spreadsheets as optional one-time offline references only).

Context: RuleTemplate remains a list row per template; facet match data for methods lives in MethodFacet (seeded from data/create/customRule/*.json). See template-recommendation-matrix.md.

Implementation (shipped):

  1. Authoring contract + storage: Zod-validated JSON under data/create/customRule/; seed into MethodFacet (and related); documented in template-recommendation-matrix.md.
  2. APIs: GET /api/templates?facet.* returns { templates, scores? }; GET /api/create-flow/methods?section=…&facet.* returns methods with match scores for card re-ranking.
  3. UI (wizard): useFacetRecommendations + rankMethodsByScore / deriveCompactCards on the custom-rule card / right-rail steps (recommended methods).
  4. Not shipped here: Marketing template grids (home, /templates) still call GET /api/templates without facets — product/UI follow-up CR-93. Automated tests for template ranking via /api/templates are deferred to CR-93 with that UI work.

Acceptance criteria:

  • Updating committed facet JSON + re-seeding changes recommendations without hand-editing arbitrary Prisma rows for facet data.
  • API behavior documented in template-recommendation-matrix.md §9; facet parsing / method route covered by tests; template ranking tests deferred per CR-93.
  • Invalid / partial facet combinations degrade gracefully (curated / score-0 fallbacks).

Files: prisma/schema.prisma, data/create/customRule/, app/api/templates/route.ts, app/api/create-flow/methods/route.ts, lib/server/ruleTemplates.ts, lib/server/validation/methodFacetsSchemas.ts, app/(app)/create/hooks/useFacetRecommendations.ts, docs/guides/template-recommendation-matrix.md.

Status: CR-88 Done.


Ticket 22 — Public catalog API (templates + methods + core values; CR-115)

Depends on: Ticket 16 / CR-88 (facet ranking).

Goal: Machine-readable HTTP catalog so external clients and tools can discover curated templates and all built-in governance methods (including core values) without importing messages/en/create/customRule/*.json.

Implementation (shipped):

  1. lib/server/governanceCatalog.ts — DTOs from messages JSON (English v1).
  2. GET /api/templates/[slug] — template + templateMethodsFromBody composition.
  3. GET /api/create-flow/methods — extended: full copy, full deck, coreValues (+ values alias), facet merge unchanged for method sections.
  4. lib/create/fetchTemplates.tsfetchTemplateDetailBySlug; fetchTemplateBySlug uses detail route.
  5. Tests: governanceCatalog.test.ts, templatesBySlugRoute.test.ts, extended createFlowMethodsRoute.test.ts.

Acceptance criteria:

  • GET /api/templates/[slug] returns one template or 404.
  • GET /api/create-flow/methods?section=coreValues returns all presets with stable ids.
  • Method sections return full metadata; facet ranking preserved.
  • Documented in CONTRIBUTING.md and template-recommendation-matrix.md §9.

Files: lib/server/governanceCatalog.ts, app/api/templates/[slug]/route.ts, app/api/create-flow/methods/route.ts, lib/server/ruleTemplates.ts, lib/create/fetchTemplates.ts, lib/create/customRuleFacets.ts.

Status: CR-115 Done (in-repo).


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. Wizard screens render from app/(app)/create/[screenId]/page.tsx plus CREATE_FLOW_SCREEN_REGISTRY (Figma node + layout family per slug). docs/create-flow.md is the canonical URL/persistence summary: Create Community (eight semantic steps ending at review) → Create Custom CommunityRuleReview 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 prefill (skipping straight to publish) is out of scope here (future ticket). The Customize button on /create/review-template/[slug] now prefills customize selections and routes to core-values (or informational when Community is empty) via buildTemplateCustomizePrefill. Use without changes writes template.body.sections into state.sections and routes to confirm-stakeholders, so the user exits through the normal final-review → handleFinalize → publishRule pipeline and inherits its 401 sign-in gate. When either button is clicked before the community stage is done, the handler still applies its side effects eagerly and pins a pendingTemplateAction: { slug, mode } on CreateFlowState; CommunityReviewScreen consumes the pin on mount and router.replaces past itself to the right downstream step (core-values for customize, confirm-stakeholders for useWithoutChanges), so users never see the community-review page after expressing template intent at the template-review step.

Implementation:

  1. Keep docs/create-flow.md in sync with product/Figma (stage ↔ step mapping, future template routes).
  2. Remove legacy app/(app)/create/[step]/page.tsx — replaced by app/(app)/create/[screenId]/page.tsx with real screens; unknown slugs notFound().
  3. Unify step source of truth / **resume after SignedInDraftHydration — moved to CR-86 (profile lists drafts, continue at last step, new rule vs server draft; see docs/create-flow.md known gaps).
  4. Wire 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.
  5. 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 matches shipped behavior or lists known gaps, including Figma three-stage mapping and future template route note; draft/hydration follow-ups point to CR-86.
  • No misleading dynamic step placeholder for valid wizard URLs.
  • Footer progress reflects step index via getProportionBarProgressForCreateFlowStep (optional stage labels still Figma-dependent).
  • Template Customize prefill is documented (maps template body to selected*Ids + coreValuesChipsSnapshot, routes to core-values when Community has data else informational); full template-customize-from-mid-wizard entry beyond core-values stays deferred.

Files: docs/create-flow.md, app/(app)/create/, app/components/navigation/CreateFlowFooter/, optionally docs/backend-roadmap.md §12 cross-links.

Linear: CR-89 Done. Open-ended draft/hydration work: CR-86. Parallel to templates (78) and publish (6); not part of CR-72 → CR-83.


Ticket 18 — Invite stakeholders (email) from confirm-stakeholders step

Depends on: Ticket 3 / CR-74 + Ticket 4 / CR-75 (magic-link + session) for per-stakeholder identity, Ticket 6 / CR-77 (publish) so a PublishedRule exists to invite people to, Ticket 2 / CR-73 (CreateFlowState validation) to land the persisted stakeholder shape.

Server / admin: Same as CR-74 — dev works against Mailhog / dev-log; staging/prod reuses the SMTP + SPF/DKIM already set up for magic-link email.

Goal: Turn the confirm-stakeholders step from a local chip list into a real stakeholder invite feature. Today the screen only collects in-memory ChipOption[] labels with no email, no persistence, no notification. Product copy in messages/en/create/reviewAndComplete/confirmStakeholders.json already promises that "Adding people at this step will invite them to see your proposed CommunityRule and make their own proposals" — we need the feature behind that promise.

Context: No Figma hand-off or product direction yet for the invite mechanic. Email is the working assumption (magic-link parity) but could become email + copy-link, in-app invite, SMS, etc. CreateFlowState has no structured stakeholders field and there is no RuleStakeholder / RuleInvite Prisma model. Leaving the step as-is ships a copy promise we do not keep; this ticket is the container for closing that gap once the design + product brief lands.

Open product questions (block implementation until resolved):

  1. Invite channel: email only, email + shareable link, or in-app (account required)?
  2. Does accepting an invite require a CommunityRule account (magic-link), or can stakeholders read / propose anonymously?
  3. Per-stakeholder permissions: view-only vs. propose-changes vs. co-owner? Default?
  4. Timing: invites sent on publish (after POST /api/rules) or at confirm-stakeholders save? How are post-publish additions handled?
  5. Revocation / re-send UX and rate limits (reuse magic-link per-IP limiter or a new per-rule limiter?).
  6. Notification copy + sender identity (reuse SMTP_FROM or dedicated sender).

Implementation sketch (subject to product sign-off):

  1. Product + design pass to answer the questions above and produce Figma for the stakeholder row (email field, validation, resend/remove affordances, empty state).
  2. Schema: Add a structured stakeholders: StakeholderInvite[] to CreateFlowState (Ticket 2 pattern) and/or a Prisma RuleStakeholder (id, ruleId, email, role, invitedAt, acceptedAt, revokedAt, tokenHash?). Decide draft vs. published semantics.
  3. API: Likely POST /api/rules/:id/stakeholders (invite), DELETE /api/rules/:id/stakeholders/:id (revoke), POST /api/rules/:id/stakeholders/:id/resend. Validate with Zod; align error shape with Ticket 13 / CR-84.
  4. Mail: Reuse lib/server/mail.ts + Mailhog/dev-log pattern from CR-74; per-stakeholder hashed token akin to MagicLinkToken if acceptance requires identity.
  5. UI: Replace free-text chips with email + optional label input, client-side email validation, inline errors (invalid email, 429 retryAfterMs, duplicate). Use markCreateFlowInteraction() per the create-flow guardrails in .cursor/rules/create-flow.mdc.
  6. i18n: Expand confirmStakeholders.json with invite copy (send button, error strings, resend, revoke confirm).
  7. Tests: Schema + route unit tests; Vitest component tests for the form; optional E2E once publish is stable.

Out of scope:

  • Real-time collaboration / editing by stakeholders.
  • Threaded proposals / comments UI (future product work).
  • Notifications beyond the initial invite (digest emails, reminders).

Acceptance criteria:

  • Product + design brief answering the open questions, linked from the Linear issue.
  • CreateFlowState.stakeholders (or equivalent) is typed, validated server-side, and persists across draft save / resume.
  • An invited stakeholder receives the chosen notification (email in the default proposal) in local dev via Mailhog / dev-log, same pattern as magic-link.
  • Invalid / duplicate / rate-limited invites show clear, accessible errors; no silent failures.
  • Copy in confirmStakeholders.json matches the shipped capability (no "will invite" promise without implementation).

Files (expected): app/(app)/create/screens/select/ConfirmStakeholdersScreen.tsx, messages/en/create/reviewAndComplete/confirmStakeholders.json, app/(app)/create/types.ts, prisma/schema.prisma, new app/api/rules/[id]/stakeholders/*, lib/server/mail.ts, lib/server/validation/, tests under tests/.

Linear: CR-90 (Backlog). Parallel to the CR-72 → CR-83 chain; unblocked to start the product/design brief now — implementation waits on CR-74 / CR-75 / CR-77 / CR-73.


Ticket 19 — Add button behavior on custom-rule pages and Final Review chips

Depends on: nothing technical. Pure product + design clarification — blocks consistent UX of the Create flow's custom path and the Final Review screen, but does not block any backend work.

Server / admin: none.

Goal: Decide what the Add affordance should do on each custom-rule page and on the Final Review screen, and produce Figma covering the chosen behavior. Today the affordance is inconsistent: on most custom-rule pages it just expands the card list, on core-values it opens a real modal flow, and on Final Review the + button on each chip row is a no-op.

Context — what Add does today:

  • Core values (CoreValuesSelectScreen.tsx) — full custom-chip flow: Add value → empty chip with input → check → opens an editable meaning / signals modal. Dismissing the modal now drops the brand-new chip (customPending session). Add Value confirms it as a selected chip.
  • Communication / Membership / Conflict management / Decision approaches (card-style screens, e.g. CommunicationMethodsScreen.tsx) — there is no Add custom method affordance. The inline add link in the page description (messages/en/create/customRule/*.json, compactDescriptionLinkLabel: "add") only toggles setExpanded(true) on the card stack — it shows more preset cards, it does not open a creation modal.
  • Confirm stakeholders — email chips persisted as stakeholderEmails; invites sent at first publish; see docs/create-flow.md § Stakeholder emails and Ticket 18 / CR-90.
  • Community structure — multiselect-style add (free-text chip).
  • Final Review (FinalReviewScreen.tsx) — renders <Rule categories=…> and only wires onChipClick. category.onAddClick is not provided, so the + button on each MultiSelect category renders by default (addButton={!hideCategoryAddButton} in Rule.view.tsx) but does nothing when clicked. Dead control we are shipping today.

Open product questions (block implementation until resolved):

Section A — custom-rule card pages (communication, membership, conflict management, decision approaches):

  1. Should each of these pages have a true "Add custom <method/approach/protocol>" button, distinct from the existing add text link that just expands the card stack?
  2. If yes: where does it sit (header CTA / end of stack / both)? Does it open a clean empty modal in the same shape as the page's edit modal (the *EditFields components under app/(app)/create/components/methodEditFields/)? Same customPending semantics as core values (dismiss = drop, confirm = persist + select)? Required vs optional fields?
  3. If no: should we rename or remove the inline add link in the description so it doesn't promise creation behavior we don't deliver?

Section B — Final Review screen + button per category:

  1. Pick one:
    • Option 1 — fresh modal in place. Clicking + opens FinalReviewChipEditModal seeded empty for the category. On Save, append a new chip and write through to the matching *MethodDetailsById (or coreValueDetailsByChipId + snapshot) map, matching the editable chip flow that just shipped.
    • Option 2 — bounce back to the source step. Navigate the user to the corresponding custom-rule step (e.g. communication chip +/create/communication) so they add via the existing card-page flow, then return to Final Review.
    • Option 3 — hide the button. Pass hideCategoryAddButton to the Final Review <Rule> so the dead control disappears until product knows what they want.
  2. If Option 1, what is the empty state of the modal (title only, all fields, recommended seeds)?
  3. If Option 2, how do we preserve the user's place on Final Review so they can come back without re-confirming everything?
  4. Does the answer differ for coreValues vs the four method categories? (Different edit-fields components, different state slices.)

Implementation sketch (subject to product sign-off):

  1. Product + design pass answering Sections A and B; produce Figma for whichever options win on each affected page and on Final Review.
  2. Custom-rule card pages (if Option A1 wins): add an Add custom <method> button alongside the card stack on each of the four pages. Reuse the page's *EditFields component inside a customPending-style modal (same dismiss-drops / confirm-persists semantics as CoreValuesSelectScreen). Persist into the matching *MethodDetailsById slice and selected-id list.
  3. Final Review (if Option B1 wins): wire category.onAddClick per category in FinalReviewScreen.tsx → open FinalReviewChipEditModal seeded empty for the matching groupKey, with a customPending mode that drops the new entry on dismiss and persists on Save (parity with the current chip-edit flow).
  4. Final Review (if Option B3 wins, interim): pass hideCategoryAddButton to the Final Review <Rule> so the no-op + is removed.
  5. i18n: add copy for the new buttons, modal headers, validation, and confirmation/discard prompts under messages/en/create/customRule/*.json and messages/en/create/reviewAndComplete/finalReview.json.
  6. Tests: Vitest component tests covering dismiss-drops vs confirm-persists for each new modal (mirror the new CoreValuesSelectScreen.test.tsx custom chip — confirmed vs dismissed pair).

Out of scope:

  • Server-side persistence changes — adding a chip already flows through existing *MethodDetailsById / coreValueDetailsByChipId snapshots.
  • Reworking the inline add link's compactDescriptionLinkLabel semantics until product decides whether it still belongs.
  • Bulk-add UX (paste many chips at once).

Acceptance criteria:

  • Product brief answering Section A and Section B, linked from the Linear issue.
  • Figma covering whichever options product picks for the four card-style pages and Final Review.
  • If Option B3 (interim) ships first: the Final Review + is no longer rendered (no dead controls).
  • Once Option A1 / B1 are picked: implementation tickets file under this one with customPending parity and tests, no regressions to the existing chip-edit flow.

Files (reference): app/(app)/create/screens/card/CommunicationMethodsScreen.tsx, MembershipMethodsScreen.tsx, ConflictManagementScreen.tsx, right-rail/DecisionApproachesScreen.tsx, select/CoreValuesSelectScreen.tsx, review/FinalReviewScreen.tsx, components/FinalReviewChipEditModal.tsx, components/methodEditFields/, components/cards/Rule/Rule.view.tsx.

Linear: CR-91 (Backlog). Sibling product/design ticket to CR-90 (same "copy promises a feature, UI doesn't deliver" pattern). Parallel to the CR-72 → CR-83 chain; not on the critical path.


Ticket 20 — Change account email (verified new address)

Depends on: Ticket 15 / CR-86 (signed-in profile shell); Ticket 3 / CR-74 (magic-link + mail patterns).

Server / admin: Same SMTP / DNS expectations as magic-link when email must work on staging/production (see Ticket 3 table).

Goal: Let a signed-in user change their login email after verifying control of the new address (magic-linkstyle flow). Today User.email is unique and magic-link verify upserts on that field; profile shows “Change email — coming soon” only.

Context: Explicitly out of scope for Ticket 15; this ticket is the dedicated backend + product slice. See docs/backend-roadmap.md §1 / §6. Canonical Linear body: CR-103.

Implementation (sketch):

  1. Persistence: Pending email-change token (userId, newEmail, tokenHash, expiresAt)—separate from sign-in MagicLinkToken or clearly discriminated so flows cannot be confused.
  2. API: Authenticated request (submit new email → mail link); verify (token → update User.email in a transaction, cleanup pending rows).
  3. Conflicts: If newEmail already belongs to another User, return a clear error (merge accounts out of scope unless product decides otherwise).
  4. Sessions: Decide whether successful change invalidates other sessions for that user; document in code + roadmap.
  5. Rate limits: Align with app/api/auth/magic-link/request/route.ts patterns.
  6. Mail: Distinct template/copy from sign-in in lib/server/mail.ts.
  7. UI: Replace profile “coming soon” with real flow; i18n under messages/.
  8. Tests: Route tests for happy path, expired/invalid token, duplicate email, unauthenticated request.

Acceptance criteria:

  • New email is confirmed only after the user completes the link sent to that inbox; then User.email updates.
  • Duplicate-email and rate-limit cases are handled with accessible errors (CR-84 shape).
  • Profile reflects the new address after success.
  • Documented session policy after email change.

Files (expected): prisma/schema.prisma, new app/api/user/... or app/api/auth/... routes, lib/server/mail.ts, app/(app)/profile/, messages/en/pages/profile.json, tests under tests/unit/.

Linear: CR-103 (Backlog). Related: CR-86 (profile); CR-84 (errors).


Ticket 21 — Create flow file uploads (community photo + custom method attachments)

Depends on: Ticket 3 / CR-74 (session for authenticated routes); Ticket 5 / CR-76 (draft JSON carries new URL fields). Related: CR-58 (Figma Avatar / broader upload routing — may share /api/uploads later).

Server / admin: Persist uploads on disk under UPLOAD_ROOT (document in .env.example); production aligns with Cloudron localstorage mount (docs/guides/ops-backend-deploy.md). Not storing binaries inside RuleDraft.payload / publish JSON — only HTTPS-relative URLs (or opaque ids served by the app).

Goal: Replace filename-only / stub upload UX with real files for (1) Create Community community-upload and (2) custom method wizard / modal upload field blocks. Draft and publish JSON carry stable URLs like other strings.

Context (repo today):

Implementation (sketch):

  1. lib/server/uploads/ — save stream to UUID filename under UPLOAD_ROOT; resolve path safely (no traversal).
  2. POST /api/uploadsapiRoute, multipart formData, purpose enum (communityAvatar | customMethodAttachment or similar), MIME + size allowlists, optional rate limit alignment with magic-link patterns.
  3. GET /api/uploads/[id] (or equivalent) — stream file by opaque id; document public vs session-gated read policy for v1.
  4. Client: thin uploadCreateFlowFile(file, purpose) (e.g. under lib/create/); i18n for errors; loading UX per alerts rules.
  5. State + Zod: communityAvatarUrl (name TBD) on CreateFlowState; upload blocks gain assetUrl (keep fileName for display); createFlowSchemas.ts updated.
  6. UI: wire CommunityUploadScreen; wizard + CustomMethodCardFieldBlocksSummary.
  7. Publish: extend buildPublishPayload.ts / document if product needs URLs for readers; else TODO in ticket / PR.

Acceptance criteria:

  • Signed-in user completes upload on community step; URL persists in draft when sync is on.
  • Custom-method upload block stores assetUrl after upload; visible in modal / final review paths.
  • Anonymous flow does not claim server upload without session; agreed staging strategy works or picker is gated with copy.
  • Unit tests: validation, path safety; targeted Vitest for happy path where practical.

Linear: CR-113 (Backlog).

Internal plan: Cursor plan create_flow_real_uploads_ebeecca5.plan.md (workspace .cursor/plans/) expands sequence diagrams and rollout order.


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 stops relying on ephemeral files under .next/web-vitals in production.

Context: Multi-instance / Docker loses file-based metrics. docs/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/(admin)/monitor/page.tsx, optionally prisma/schema.prisma only if option C.

Repo check (2026-04): Route still writes files under .next/web-vitalsCR-80 remains applicable.


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.

Repo check (2026-04): Only app/api/rules/route.ts exists (list + POST); no app/api/rules/[id]/ handler yet — CR-81 still open.

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. Related in Linear: CR-86 (Ticket 15 — profile cards linking to public detail).


Ticket 11 — Database migration smoke (local)

Depends on: nothing in production; Docker on the developer machine.

Goal: Catch broken SQL migrations before merge.

Implementation (shipped in repo): npm run migrate:smoke runs scripts/migrate-smoke-local.sh — ephemeral Postgres on 127.0.0.1:5433, prisma migrate deploy, connection check, teardown. Documented in CONTRIBUTING.md and docs/testing-guide.md § Running tests (Prisma). In-repo Gitea workflow YAML for this was removed in favor of the local script; reintroducing a remote job is optional if self-hosted runners are available again.

Acceptance criteria:

  • Documented + scripted local migrate smoke before merge.
  • (Optional) Remote CI job, if you later restore runners and want parity.

Files: scripts/migrate-smoke-local.sh, package.json (migrate:smoke), CONTRIBUTING.md, docs/testing-guide.md.


Ticket 12 — Staging / production admin handoff (Cloudron at MEDLab)

Depends on: Tickets 18 complete enough to deploy a vertical slice.

Server / admin: Cloudron admin access on my.medlab.host granted. Scope of this ticket is the handoff doc + cutover plan — exactly what's in place, what the side-by-side cutover looks like, and what open product/infra questions remain. The steady-state operator runbook is split out into CR-100 (we write it after we've done the work).

Goal: Short doc that captures (a) granted access + auto-injected vs. manually-set env vars + platform settings, (b) the side-by-side → apex cutover plan with the legacy communityrule.info service, and (c) the remaining open questions (apex vs. permanent-subdomain final URL, legacy rules data communication, container registry choice).

Platform context: Target is Cloudron at MEDLab (my.medlab.host). The legacy communityrule.info is a single Cloudron LAMP app (lamp.cloudronapp.php74@5.1.2, 512 MiB at apex) hosting three things stuffed into one container under /app/data/public/: the static marketing site, the Express/MySQL backend at CommunityRule/CommunityRuleBackend (kept alive by a 30-min run.sh watchdog on port 3000; MySQL is the LAMP package's bundled MySQL, not a Cloudron addon), and the Flask chatbot at CommunityRule/CommunityRuleChatBot (currently crash-looping with ModuleNotFoundError, last touched May 2024). New app is a properly packaged Cloudron app (Docker image + CloudronManifest.json, postgresql + sendmail + localstorage addons) and replaces all three — no data migration. Cloudron's container supervisor replaces the watchdog.

Implementation (shipped):

  1. docs/guides/ops-backend-deploy.md:
    • §1 Context — what the legacy LAMP slot actually contains and why side-by-side cutover is the safe path.
    • §2 Access — what Cloudron admin already grants self-serve; only outstanding admin-side step is generating a CLI token.
    • §3 Env vars split into Cloudron auto-injected (CLOUDRON_POSTGRESQL_URL, CLOUDRON_MAIL_SMTP_*) vs. manually-set (SESSION_SECRET, SMTP_FROM, NEXT_PUBLIC_ENABLE_BACKEND_SYNC). Notes that addons are manifest-declared, not platform-enabled, and that platform mail is SES-relayed on communityrule.info with custom-from allowed.
    • §4 Platform settings (httpPort: 3000, healthCheckPath: /api/health, 512 MiB to start, automatic backups already on).
    • §5 Cutover plan — staging at staging.communityrule.info, soft-launch, apex cutover at scheduled low-traffic window (~515 min downtime).
    • §6 Open questions — apex vs. permanent subdomain final URL; legacy rules data communication; container registry choice.
    • §7 Old vs new deltas (LAMP-package detail, watchdog, OTP→magic link, sender, API surface, chatbot).
    • §8 Follow-up tickets (the six tickets below).
  2. Cross-links: docs/guides/backend-roadmap.md §11 (environments — names Cloudron at MEDLab) and §8 (migrations policy — never rewrite applied migrations).

Acceptance criteria:

  • Admin handoff covers exactly the access that was needed (most self-serve via Cloudron admin login).
  • Cutover plan is side-by-side and explicitly avoids in-place apex replacement.
  • Six follow-up tickets enumerated and linked, with CR-99 + CR-101 scope corrected to reflect that legacy is one LAMP slot containing marketing + backend + chatbot (all retire together).
  • Open product/infra questions surfaced rather than assumed.

Files: docs/guides/ops-backend-deploy.md, docs/guides/backend-roadmap.md, docs/README.md, CONTRIBUTING.md.

Status: CR-83 Done. Deployment-pipeline implementation tracked in the follow-up tickets below.

Follow-up tickets filed under CR-83

All six are titled [Backend] …, assigned to Vinod, in the community-rule team, Backlog state. IDs filled in once filed via Linear MCP.

# Linear Title Depends on
1 CR-96 [Backend] Cloudron-native env vars Done — merged
2 CR-97 [Backend] Container image registry: choose, build, push Done — first image 0.1.0 verified
3 CR-98 [Backend] Cloudron staging install + smoke Cloudron CLI token (§2) — next
4 CR-99 [Backend] Cloudron production install + apex cutover CR-98 green for the agreed overlap window
5 CR-100 [Backend] Steady-state operator runbook CR-98 (write what we actually did)
6 CR-101 [Backend] Decommission legacy CommunityRule LAMP app CR-99 + sign-off window
7 CR-102 [Backend] Decide fate of legacy rules table (read-only export?) must resolve before CR-99 maintenance window

PR plan (CR-96 CR-102)

One ticket ≠ one mega-branch. Repo PRs stay small; stack only when there is a hard code dependency (e.g. CR-97 before CR-98 can pull a built image from main). Ops steps (Cloudron install, DNS, smoke, cutover, decommission) are tracked in Linear checklists — not bundled into a single git branch.

Order Linear Repo PR / branch Kind Status Blocked by
1 CR-96 adilallo/Backend/BridgeCloudronEnv[Backend] Cloudron-native environment variables repo Done
2 CR-97 Container registry packaging + docker-release.sh repo Done
CR-102 TBD — optional repo PR if export tooling/docs needed product / repo Parallel row count from legacy MySQL (preCR-99 backup)
3 CR-98 — (ops checklist; ops-backend-deploy.md §10) ops Next Cloudron CLI token only
4 CR-100 TBD — docs/guides/ops-runbook.md docs Backlog CR-98 (write what we actually did)
5 CR-99 — (ops; maintenance window) ops Backlog CR-98 green + CR-102 resolved
6 CR-101 — (ops; uninstall LAMP slot) ops Backlog CR-99 + sign-off window

What's next: CR-98 — staging install + smoke at staging.communityrule.info (ops-backend-deploy.md §10). Start CR-102 product decision in parallel so it is resolved before the CR-99 cutover window.

Per-ticket detail:

  1. Cloudron-native env vars (CR-96). Done. App reads CLOUDRON_POSTGRESQL_URL and CLOUDRON_MAIL_SMTP_* only (no DATABASE_URL / SMTP_URL shim). Prisma datasource uses CLOUDRON_POSTGRESQL_URL. Local dev uses the same names in .env. SMTP URL assembled in lib/server/env.ts; mail senders use getSmtpUrl(). scripts/start.sh does not bridge env names.
  2. Container image registry (CR-97). Done. Gitea registry on git.medlab.host; repo CommunityRule/community-rule is public (package visibility inherits from repo). Image git.medlab.host/communityrule/community-rule:0.1.0, built linux/amd64 via ./scripts/docker-release.sh. Anonymous pull verified. CI on merge to main deferred (no hosted runners).
  3. Cloudron staging install + smoke (CR-98). Next. Full checklist in ops-backend-deploy.md §10. Summary:
    • Prereqs: Cloudron CLI token (only outstanding item); CR-96 + CR-97 done.
    • Install: cloudron install --location staging.communityrule.info from repo root (manifest supplies dockerimage; migrations run in start.sh).
    • Configure: SESSION_SECRET, SMTP_FROM, NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true, UPLOAD_ROOT=/app/data/uploads.
    • Acceptance: GET /api/health{"ok":true,"database":"connected"}; magic-link sign-in end-to-end; publish a rule succeeds.
  4. Cloudron production install + DNS cutover. Acceptance: production subdomain resolves to the new app; old subdomain still works during overlap; sign-in + publish succeed against production; backups confirmed.
  5. Steady-state operator runbook. Lives at docs/guides/ops-runbook.md (sibling to the handoff). Covers deploy a new version, rollback, restore drill cadence, multi-instance limitations from backend-roadmap.md §5/§7. Acceptance: a fresh reader can deploy + roll back using only this doc.
  6. Decommission legacy Express/MySQL backend. Acceptance: old Cloudron app stopped + uninstalled; old MySQL addon backed up once and removed; legacy Gitea repo README updated to point at this app. Priority: Low.

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 §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/responses.ts, lib/server/requestId.ts, lib/server/apiRoute.ts; migrated app/api/auth/**/route.ts, app/api/drafts/me/route.ts, app/api/rules/route.ts, app/api/rules/[id]/route.ts; tests in tests/unit/{responses,requestId,apiRoute,draftsMeRoute,rulesByIdRoute}.test.ts.

Linear: CR-84 Done (CR-73 Done — was 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 §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, app/api/auth/magic-link/verify/route.ts, optional prisma migration if new columns (unlikely).

Linear: CR-85 Done — multi-device policy; lazy expired-row cleanup on every sign-in (per-user prune via @@index([userId]) + ~5% global sweep); no rotation in v1; cleanup failures logged but never fail sign-in. ADR comment block lives at the top of lib/server/session.ts; no Prisma migration needed.


Ticket 15 — Profile dashboard + account (Figma profile)

Depends on: Ticket 3 (auth), Ticket 4 (session in UI), Ticket 6 (CR-77 publish — Done — so real published rules exist for “my rules”). Soft optional: Tickets 78 for “create from template” CTA parity.

Goal: Signed-in profile experience matching Figma — Profile: 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): Ticket 20 / CR-103—not part of this slice until that issue ships. Until then, product may keep “Coming soon” or hide the row.
  • 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: GET /api/rules remains a public list; authenticated owner APIs (GET /api/rules/me, DELETE /api/rules/[id], POST …/duplicate, DELETE /api/user/me, stakeholders, email-change) are shipped — see docs/backend-roadmap.md §1 profile table and CONTRIBUTING.md. CR-86 tracks profile UI completion.

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 in this ticket (tracked in CR-103 / Ticket 20); Figma row handled per product (hide/disabled/coming soon).

Files: new app/ routes and components, app/api/rules/... (or new segment handlers), lib/create/api.ts as needed, prisma/schema.prisma only if account-delete policy requires schema tweaks, messages/en/ for copy.

Linear: CR-86 (Backlog). Publish prerequisite: CR-77 Done — not blocked on wiring. Related: CR-81 (public rule detail for deep links from profile cards). Not part of the sequential CR-72 → CR-83 chain—parallel after session, similar to CR-84/CR-85. Also on CR-86: draft list + resume + hydration alignment (moved from CR-89).


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 Migrate smoke (local) Done in repo
12 12 Ops admin handoff (Cloudron) Done
13 13 API errors + request-id logging
14 14 Session lifecycle + cleanup
15 15 Profile + account (Figma profile)
16 16 Template matrix (facets; no xlsx)
17 17 Canon create-flow (custom path)
18 18 Stakeholder invites (confirm-stakeholders)
19 19 Add button behavior (custom-rule pages + Final Review)
20 20 Change account email (verified) Backlog — CR-103
21 21 Create flow file uploads Backlog — CR-113

Follow-up (no doc ticket #): CR-93 — marketing template grids ranked by user facets (API-ready; tests deferred with that issue).

Tickets 1011 can be deferred without blocking the core “auth + drafts + publish + templates” vertical slice. Ticket 6 / CR-77 (publish) is Done. Ticket 16 / CR-88 (facet data + APIs + wizard method ranking) shipped after 78; CR-93 tracks marketing template grids ranked by user facets (API-ready). Ticket 17 / CR-89 (Done) canonizes the custom wizard in docs/create-flow.md (progress bar, [screenId] routing). Draft resume / hydration follow-ups: CR-86. Tickets 1314 are parallel (CR-84 / CR-85 — both Done). Ticket 15 / CR-86 is parallel (publish prerequisite met); implementation backlog. Ticket 20 / CR-103 tracks verified change account email (split from Ticket 15). Ticket 18 (CR-90) adds real email-based stakeholder invites to the confirm-stakeholders step — currently ships as a label-only chip list despite copy promising invites; parallel to the main chain, awaits design + product brief before implementation. Ticket 19 (CR-91) is a product/design clarification ticket: the Add affordance is inconsistent across custom-rule pages (full custom-chip flow only on core-values; an add link that just expands the card stack on the four card-style pages) and the Final Review screen renders a + button per category that today is a no-op; needs a brief + Figma before any implementation lands. Ticket 21 (CR-113) — create-flow authenticated file uploads for community photo + custom-method attachment blocks; see Ticket 21 body in this doc. Implementation follows docs/guides/ops-backend-deploy.md localstorage guidance and avoids unauthenticated multipart in v1.


Linear (Community-rule team)

Main chain (historical): CR-72 → CR-83 was the original strict sequence; repo + Linear status today: CR-72CR-79, CR-83, CR-84, CR-85, CR-88, CR-89 are Done; CR-77 (publish) Done; CR-80CR-81 remain Backlog (web vitals, public rule detail). CR-82 covered by local migrate:smoke (see Ticket 11). CR-83 (admin handoff) shipped as a narrow handoff sheet; the Cloudron deployment pipeline is split into [Backend] follow-ups CR-96CR-102 — see PR plan (CR-96 CR-102) under Ticket 12 (CR-96 + CR-97 Done; CR-98 next). Parallel (still open): CR-86 / Ticket 15 (Backlog — publish not a blocker); CR-103 / Ticket 20 (change account email); CR-93 (Backlog); CR-90 / Ticket 18 (stakeholder invites); CR-91 / Ticket 19 (Add button behavior); Ticket 21 / CR-113 (create-flow file uploads).

Doc ticket Linear Title (short) Deploy PR / tracking
1 CR-72 Align backend-roadmap
2 CR-73 CreateFlowState + API validation
3 CR-74 Magic-link sign-in UI (Ticket 3; Done)
4 CR-75 Create flow session UI (Ticket 4; Done)
5 CR-76 Draft sync hardening (PUT UX / errors)
6 CR-77 Publish wiring Done
7 CR-78 Template seed Done
8 CR-79 Templates in UI
9 CR-80 Web vitals (prefer external)
10 CR-81 Public rule detail (optional)
11 CR-82 Local migrate smoke (Done in repo; optional remote CI)
12 CR-83 Ops admin handoff (Cloudron) Done
12.1 CR-96 Cloudron-native env vars Done
12.2 CR-97 Container image registry + CI Done — image 0.1.0
12.3 CR-98 Cloudron staging install + smoke Nextops-backend-deploy.md §10
12.4 CR-99 Production install + apex cutover Ops — after CR-98 + CR-102
12.5 CR-100 Steady-state operator runbook Docs PR — after CR-98
12.6 CR-101 Decommission legacy LAMP app Ops — after CR-99 + sign-off
12.7 CR-102 Legacy rules table fate / export Parallel — before CR-99
13 CR-84 API errors + request-id logging
14 CR-85 Session lifecycle + cleanup Done
15 CR-86 Profile + account (Figma 22143:900069)
16 CR-88 Template matrix (facets; no xlsx)
17 CR-89 Canon create-flow (custom wizard + docs) Done
CR-93 Template grid + facet ranking (product)
18 CR-90 Stakeholder invites (confirm-stakeholders)
19 CR-91 Add button behavior (custom-rule + Final Review)
20 CR-103 Change account email (verify new address) Backlog
21 CR-113 Create flow file uploads (community + custom blocks) Backlog

Linear sync notes (CR-74 / CR-75)

CR-74 and CR-75 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.