Skip to content

feat: harden feedback→Notion sync (central DB, outbox, reconciler, screenshots, after()) - #137

Merged
dommango merged 1 commit into
mainfrom
feat/feedback-notion-hardening
Jul 4, 2026
Merged

dommango merged 1 commit into
mainfrom
feat/feedback-notion-hardening

Conversation

@dommango

@dommango dommango commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Reworks the in-app feedback → Notion mirror onto the shared central "📥 App Feedback" database, with a durable outbox + reconciler so a submission is never silently lost, and moves the Notion write out of the request path.

  • Schema/migration (20260703000000_feedback_notion_outbox): adds notionPageId, notionSyncedAt, notionSyncAttempts, notionLastError to Feedback; partial index on unsynced rows; backfills notionSyncedAt = createdAt for rows that already carry a notionUrl. Not applied to any live DB here.
  • Pure payload mapping — extracted lib/notion/feedback-payload.ts (env-free, unit-tested), per the repo's pure-core convention (rate-limit-core.ts). Central schema: Title ({emoji} {title}), App=HessFest, Type (BUG→Bug, IDEA→Request, OTHER→Feedback), Status=New, Environment, App Row ID, conditional Page URL/User Email/Browser/Screenshot.
  • lib/notion/feedback-sync.ts rewritten for the central schema; returns { pageId, url }; TIMEOUT_MS 8s (out of request path); never throws.
  • lib/feedback/submit.ts — Notion create now runs via next/server after(); success writes notionPageId/notionUrl/notionSyncedAt, failure records notionSyncAttempts+notionLastError. Submission no longer waits on Notion.
  • Reconciler lib/notion/feedback-reconcile.ts — scans unsynced, under-cap, aged rows; queries Notion by App Row ID before creating (idempotent — backfills an existing page instead of duplicating); caps attempts at 5. Exposed at POST /api/cron/reconcile-feedback (CRON_SECRET-guarded, same guard as poll-scores), wired into scripts/cron.mjs at a 30-min cadence.
  • Public screenshot endpoint GET /api/feedback/screenshots/[id]/[index] — raster-only mime allowlist (png/jpeg/webp, no SVG), X-Content-Type-Options: nosniff, Content-Security-Policy: default-src 'none'; sandbox, Content-Disposition: inline, Cache-Control: private, max-age=3600; 404 on missing row/index/parse. Public so Notion's servers can fetch external file URLs.
  • Rate-limit fix (app/api/feedback/route.ts) — IP-less (anon) clients now share one larger feedback:anon-global bucket at 30/5min instead of starving a single 10/5min bucket; resolvable IPs keep 10/5min; signed-in users keep 10/5min.
  • Docs — adds root FEEDBACK.md (canonical contract + HessFest per-app specifics).

Test plan

  • npx vitest run — 733 passed (94 files), incl. new feedback-payload.test.ts (13) + feedback-reconcile.test.ts (7, mocked prisma/fetch/env: create→writeback, query-hit backfill/no-dupe, attempts-cap query shape, query-failure no-create, screenshot-URL build, idempotent double-run).
  • npm run typecheck — clean.
  • npm run build — compiled successfully; /api/cron/reconcile-feedback and /api/feedback/screenshots/[id]/[index] registered.
  • Manual: submit feedback in a Notion-configured env → card appears in central DB with screenshots rendering via the public endpoint; kill sync mid-flight → reconciler backfills without duplicating.

Cutover TODO (NOT done here — supervised)

  • Repoint NOTION_FEEDBACK_DB_ID on Railway to c153f1e7-b65d-49d2-a2d8-5e57d1f22b70 (central DB) and confirm NOTION_API_KEY's integration is shared with it.
  • Apply the 20260703000000_feedback_notion_outbox migration to prod (prisma migrate deploy).
  • Existing Project-Backlog feedback rows still point at the old schema/DB — they need the separate Phase-2.5 migration (not in this PR).

Notes

  • .env.example could not be edited (dotfile-protection hook blocks read+write). The two vars (NOTION_API_KEY, NOTION_FEEDBACK_DB_ID) are already declared in lib/env.ts with a comment and are documented in FEEDBACK.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_015dvV6WQgHXA7rPCYT7mtZn

…reenshots, after())

Rework the in-app feedback → Notion mirror onto the shared central "App Feedback"
database with an outbox + reconciler so submissions are never silently lost.

- schema/migration: add Feedback.notionPageId/notionSyncedAt/notionSyncAttempts/
  notionLastError outbox columns + partial unsynced index; backfill notionSyncedAt
  for rows that already have a notionUrl.
- notion sync: rewrite for the central schema; extract pure, env-free
  buildFeedbackNotionPayload (BUG→Bug, IDEA→Request, OTHER→Feedback) with unit tests.
- submit: move the Notion create out of the request path via next/server after();
  record attempts/last-error on failure instead of blocking the user.
- reconciler: lib/notion/feedback-reconcile.ts queries Notion by App Row ID before
  creating (idempotent), backfills existing pages, caps attempts at 5; exposed at
  CRON_SECRET-guarded /api/cron/reconcile-feedback, wired into scripts/cron.mjs (30m).
- screenshots: public GET /api/feedback/screenshots/[id]/[index] with a raster-only
  mime allowlist, nosniff + locked-down CSP, so Notion can fetch external image URLs.
- rate limit: give IP-less (anon) clients a larger shared 30/5min bucket instead of
  starving a single 10/5min one.
- docs: add FEEDBACK.md (canonical contract + HessFest per-app specifics).
@dommango
dommango marked this pull request as ready for review July 4, 2026 15:19
@dommango
dommango merged commit 707ab31 into main Jul 4, 2026
1 check passed
@dommango
dommango deleted the feat/feedback-notion-hardening branch July 4, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant