Files
community-rule/docs/backend-linear-tickets.md
T
2026-04-13 18:24:13 -06:00

49 KiB
Raw 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 — 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—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, 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/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/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/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 SMTP_URL, verify URL is logged; with Mailhog, use docker-compose.yml and SMTP_URL=smtp://localhost:1025.
  6. Marketing header: When signed in (fetchAuthSession), Log in becomes Profile linking to /profile (placeholder until Ticket 15 / CR-86). Implemented in TopNavWithPathname.tsx + TopNav.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/login/, 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/TopNav/TopNav.container.tsx, app/components/navigation/TopNav/TopNavWithPathname.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/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/create/layout.tsx, app/create/hooks/useCreateFlowExit.ts, app/components/utility/CreateFlowTopNav/, app/create/context/CreateFlowContext.tsx, messages/en/create/topNav.json, 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/create/hooks/useCreateFlowExit.ts, app/create/PostLoginDraftTransfer.tsx, app/create/SignedInDraftHydration.tsx, 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 already wraps POST /api/rules. UI on the final-review / completed steps (see app/create/screens/CreateFlowScreenView.tsx and app/create/screens/) must call it with { title, summary?, document } derived from CreateFlowState.

Implementation:

  1. Map useCreateFlow().statetitle / summary / document (document likely mirrors 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/create/*/page.tsx, 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).

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 after the flat list ships.

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.


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 and create entry surfaces reference future template work. Wizard URLs are static segments under app/create/; see docs/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/, create-flow entry routes under 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 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 (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/create/utils/flowSteps.ts. Wizard screens render from 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 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 in sync with product/Figma (stage ↔ step mapping, future template routes).
  2. Remove legacy app/create/[step]/page.tsx — replaced by app/create/[screenId]/page.tsx with real screens; unknown slugs notFound().
  3. Unify step source of truth: URL via useCreateFlowNavigation vs unused CreateFlowContext currentStep — pick one model; align useCreateFlowExit / draft payload if needed.
  4. Resume: After SignedInDraftHydration, decide redirect to /create/${state.currentStep} vs stay on current URL; test or document.
  5. 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.
  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 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, app/create/, app/components/utility/CreateFlowFooter/, optionally docs/backend-roadmap.md §12 cross-links.

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


Ticket 11 — CI: database migration smoke (optional, runner-dependent)

Depends on: existing .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, 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 §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 §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 (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 §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 (unblockedCR-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: 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 §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 as needed, prisma/schema.prisma only if account-delete policy requires schema tweaks, messages/en/ for copy.

Linear: CR-86 (Backlog). Blocked by CR-77 (publish) only — CR-75 Done. Related: CR-81 (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) canonizes the custom wizard in docs/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 DoneCR-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 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
7 CR-78 Template seed
8 CR-79 Templates in UI
9 CR-80 Web vitals (prefer external)
10 CR-81 Public rule detail (optional)
11 CR-82 CI migrate smoke (optional)
12 CR-83 Ops runbook / admin handoff
13 CR-84 API errors + request-id logging
14 CR-85 Session lifecycle + cleanup
15 CR-86 Profile + account (Figma 22143:900069)
16 CR-88 Template matrix + xlsx ingestion
17 CR-89 Canon create-flow (custom wizard + docs)

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.