Content Research Agent
One idea and a few sources in; a cited article plus LinkedIn, X and newsletter drafts out, behind an approval gate nothing skips.
Turning a content idea into a researched, cited article and channel posts took hours, and AI drafts tend to invent sources and skip the human.
- Idea + sources (human)
- Source type (decision)
- Firecrawl (step)
- Extract PDF·DOCX (step)
- n8n + Mistral OCR (trigger)
- Usable text? (decision)
- Excluded (thin / paywalled) (failure)
- Pick evidence (human)
- Plan + cited article: Sonnet (AI)
- 10-point scorecard (AI)
- Edit + revise (human)
- Channel drafts: Haiku · LinkedIn · X · newsletter (AI)
- Approval gate (decision)
- Newsletter via Resend (output)
- LinkedIn + X (simulated) (output)
- Supabase + RLS (store)
- Sentry (failure)
- Step 01
An editor gives an idea, source URLs, or both; audience is the only required field.
- Step 02
Firecrawl retrieves URLs; PDFs, images and DOCX extract directly, scans route through an n8n + Mistral OCR workflow.
- Step 03
Thin, boilerplate or paywalled sources are marked failed and excluded, never bypassed.
- Step 04
Sonnet plans and writes a cited article; a ten-criterion scorecard flags unsupported claims and missing citations, each linked to its quote.
- Step 05
Haiku adapts the article to LinkedIn, X and newsletter, each checked against real platform limits.
- Step 06
Approval needs a fresh passing evaluation plus all three drafts; the newsletter then sends for real via Resend.
Approval is a real gate, not a UI state
Once granted it locks every action until an admin sends it back with a required, visible reason.
A recursive RLS policy silently broke every team check
Reproduced directly in Postgres (42P17: infinite recursion in policy) and fixed with a SECURITY DEFINER helper.
Two race conditions, one bug
Double-submits and generate-while-reloading both made duplicates. A fixed delay still produced three; an atomic database claim fixed both.
Fetching a URL is an SSRF risk
Source retrieval checks the resolved destination before it ever fetches.
A recursive RLS policy silently broke every team check
Team features failed quietly with no clear error in the app.
Reproducing the exact query directly against Postgres showed 42P17: infinite recursion detected in policy, a rule on team_members that checked team_members.
Fix: A SECURITY DEFINER helper breaks the recursive chain; team membership is now enforced below the UI and tested with a real non-admin account.
If a human has to approve it, the database has to know. A disabled button is not a control.
- Map every state and who owns it (research, evaluation, editing, revision, approval, sending) before adding individual controls.
- Run the full signed-in workflow with a real editor earlier, before polishing smaller parts.
- Reserve testing and write-up time throughout the week, not at the end.