<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>MakingCode</title><description>Field notes from 14 years in enterprise software, now applied to AI/ML and business automation.</description><link>https://makingcode.io/</link><language>en-us</language><copyright>© 2026 MakingCode</copyright><item><title>Auditing 25 GitHub Repositories with Claude</title><link>https://makingcode.io/blog/auditing-25-github-repos-with-claude/</link><guid isPermaLink="true">https://makingcode.io/blog/auditing-25-github-repos-with-claude/</guid><description>How I used Claude in Chrome to audit 25 repos, spot clutter, and generate batch PowerShell scripts to clean up my GitHub profile.</description><pubDate>Tue, 04 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My GitHub account had accumulated 25 repositories over several years. It was a mix of active projects, class assignments from my master’s coursework, half-finished prototypes, and old tutorial stubs like &lt;code&gt;helloGit&lt;/code&gt; and &lt;code&gt;HelloNetlify&lt;/code&gt;. As part of building out this site and deliberately applying AI tools to real engineering workflows, I wanted to audit and organize the account. But manually clicking through 25 repository settings pages to edit descriptions, topics, and visibilities sounded like tedious administrative overhead.&lt;/p&gt;
&lt;p&gt;Instead of spending an afternoon on manual UI navigation, I used Claude in Chrome to audit the entire account and generate a batch action plan.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/profile-before.png&quot; alt=&quot;GitHub profile overview before cleanup showing 25 unorganized repositories&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The tool setup and the exact prompt&lt;/h2&gt;
&lt;p&gt;Claude didn’t have a direct API connection to my GitHub account. Rather than setting up personal access tokens or custom script integrations for a one-off task, I used Claude’s Chrome browser integration. The extension allows Claude to inspect live browser pages, navigate repository tabs, open directory trees, and read &lt;code&gt;README.md&lt;/code&gt; files directly in the browser.&lt;/p&gt;
&lt;p&gt;I opened my profile URL (&lt;code&gt;github.com/jdeleon3&lt;/code&gt;) in Chrome and ran this prompt:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Hey Claude. I&apos;d like to evaluate the state of my github repositories. I want to figure out which ones I should keep, which to delete, which ones to work on and fix. Help me get things in order. Ask any clarifying questions as you review my github. We are only doing an evaluation at this point. Here&apos;s the link: https://github.com/jdeleon3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When Claude asked clarifying questions to weight the recommendations, I specified two constraints: the primary goal was organizing the profile to highlight real engineering work, and the execution should be planned for a single focused session.&lt;/p&gt;
&lt;h2&gt;Spot-checking and verification&lt;/h2&gt;
&lt;p&gt;Rather than making blind assumptions from repository names, Claude inspected file structures and surfaced ambiguous cases for human review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;netlify-blog&lt;/code&gt;&lt;/strong&gt;: Claude flagged this as a Next.js/MDX template clone with Bejamas boilerplate and asked whether it was an active blog or an unused clone. I confirmed it was an unused template, moving it straight to the deletion bucket.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;aws-rss-reader-ui&lt;/code&gt;&lt;/strong&gt;: Claude noticed &lt;code&gt;aws-rss-reader&lt;/code&gt; had a full backend spec, while &lt;code&gt;aws-rss-reader-ui&lt;/code&gt; was an untouched Vite template. It asked if the UI was functional or stalled. I confirmed it was scaffolded and stalled, tagging it as a work-in-progress to document alongside the backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having the LLM ask before categorizing prevented deleting repositories that looked like tutorial stubs but contained custom logic.&lt;/p&gt;
&lt;h2&gt;The audit breakdown&lt;/h2&gt;
&lt;p&gt;In a single session, Claude categorized all 25 repositories into four action buckets:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Keep (active or portfolio-solid, no action needed)&lt;/strong&gt;: &lt;code&gt;MakingCode.IO.Site&lt;/code&gt;, &lt;code&gt;MakingCode.IO.ContentPipeline&lt;/code&gt;, &lt;code&gt;RD.Creations.Site&lt;/code&gt;, &lt;code&gt;Projects.RealFakeJobPredictor&lt;/code&gt;, and &lt;code&gt;ImageProcessing.BackgroundReplace&lt;/code&gt; (five repos with recent pushes, real stars, or both).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete 9 dead-weight repos&lt;/strong&gt;: Flagged zero-byte tutorial clones (&lt;code&gt;helloGit&lt;/code&gt;, &lt;code&gt;HelloNetlify&lt;/code&gt;, &lt;code&gt;HelloPython&lt;/code&gt;), &lt;code&gt;netlify-blog&lt;/code&gt; (an unused template clone, added to this bucket from the spot-check confirmation above rather than the initial pass), and abandoned stubs (&lt;code&gt;coursera-java-fundamentals&lt;/code&gt;, &lt;code&gt;azurestaticwebappwithvue&lt;/code&gt;, &lt;code&gt;Graduate.Work&lt;/code&gt;, &lt;code&gt;Projects.Infrastructure.ExplainableAnomalyDetection&lt;/code&gt;, &lt;code&gt;Projects.ExplainableNetworkMonitoring&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix missing metadata on 8 repos&lt;/strong&gt;: Identified repos with real code but blank descriptions: &lt;code&gt;rag-saas-mvp&lt;/code&gt;, &lt;code&gt;Projects.Movies.ReviewSentimentAnalysis&lt;/code&gt;, &lt;code&gt;aws-rss-reader&lt;/code&gt;, &lt;code&gt;aws-rss-reader-ui&lt;/code&gt; (the same stalled scaffold flagged during spot-checking, back again here because a stalled repo still needs an honest description), &lt;code&gt;aws-lambda-siteCrawler&lt;/code&gt;, and the three Data Mining repos (&lt;code&gt;Datamining.Classification.TumorType&lt;/code&gt;, &lt;code&gt;Datamining.Classification.CustomerSegmentation&lt;/code&gt;, &lt;code&gt;Datamining.Clustering.CreditCardDataset&lt;/code&gt;). Claude drafted custom one-line descriptions and topic tags based on actual directory contents. The eventual script also touched the four Keep-bucket repos and flipped one to private, covered below.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consolidate duplicates (2 deletions)&lt;/strong&gt;: Identified three separate stock trading repositories (&lt;code&gt;Stock-Swing-Trader&lt;/code&gt;, &lt;code&gt;Projects.Finance.StockSwingTrader&lt;/code&gt;, &lt;code&gt;Projects.ExplainableStockPredictions&lt;/code&gt;) and recommended merging them into &lt;code&gt;Stock-Swing-Trader&lt;/code&gt;, deleting the two redundant stubs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Five repos landed in Keep, but one of them was a private repository resulting in only four pinned repositories: &lt;code&gt;Projects.RealFakeJobPredictor&lt;/code&gt;, &lt;code&gt;ImageProcessing.BackgroundReplace&lt;/code&gt;, &lt;code&gt;MakingCode.IO.Site&lt;/code&gt;, and &lt;code&gt;RD.Creations.Site&lt;/code&gt;. &lt;code&gt;MakingCode.IO.ContentPipeline&lt;/code&gt; stayed a Keep without getting pinned.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/audit-document.png&quot; alt=&quot;Audit breakdown document generated by Claude categorizing all 25 repositories&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Generating executable PowerShell scripts&lt;/h2&gt;
&lt;p&gt;Rather than making manual UI edits, Claude output ready-to-run GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) commands.&lt;/p&gt;
&lt;p&gt;One immediate gotcha: Claude formatted multiline commands using Bash line continuation backslashes (&lt;code&gt;\&lt;/code&gt;). PowerShell has no line-continuation meaning for &lt;code&gt;\&lt;/code&gt; at all (only the backtick does), so a trailing backslash doesn’t continue the command onto the next line. It just ends the line early, and PowerShell parses whatever comes next as a separate, malformed statement.&lt;/p&gt;
&lt;p&gt;The next attempt was PowerShell’s actual line-continuation character, the backtick. That didn’t hold up either: a single invisible trailing space after a backtick silently breaks the continuation. PowerShell doesn’t throw a syntax error; it just stops treating the next line as part of the same command, and the failure shows up downstream as a confusing, unrelated-looking error instead of pointing at the real cause. After tripping over that once, I stopped fighting line continuation entirely and had every &lt;code&gt;gh repo edit&lt;/code&gt; and &lt;code&gt;gh repo delete&lt;/code&gt; command flattened to a single line. That’s what’s actually in all three &lt;code&gt;.ps1&lt;/code&gt; scripts below.&lt;/p&gt;
&lt;p&gt;I split the commands into three scripts in my workspace:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;delete_stale_repos.ps1&lt;/code&gt; (deleting 9 dead-weight repos):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;helloGit &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;HelloNetlify &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;HelloPython &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;coursera&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;java&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;fundamentals &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;azurestaticwebappwithvue &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Graduate.Work &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.Infrastructure.ExplainableAnomalyDetection &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.ExplainableNetworkMonitoring &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;netlify&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;blog &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;consolidate_stock_repos.ps1&lt;/code&gt; (deleting 2 redundant duplicate stubs):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.Finance.StockSwingTrader &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo delete jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.ExplainableStockPredictions &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;add_descriptions.ps1&lt;/code&gt; ended up covering more than just the 8 Fix-bucket repos: I also used it to refresh descriptions and topics on the 4 pinned Keep-bucket repos, and to flip &lt;code&gt;RD.Creations.Site&lt;/code&gt; to private in the same pass. One inconsistency worth flagging: &lt;code&gt;rag-saas-mvp&lt;/code&gt;’s description below says “Production-ready”, whereas the audit table called the same repo stalled at 5 commits. That’s the line that actually shipped; I’m not rewriting it for this post. One more note: the visibility change for &lt;code&gt;RD.Creations.Site&lt;/code&gt; ran as part of this script, but the profile and repo-list screenshots later in this post were captured before that line executed, so they still show it public (a timing artifact, not a contradiction):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# --- Descriptions + topics: pinned / Keep repos ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;MakingCode.IO.Site &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Content/blog site for the MakingCode.IO brand - Astro on Cloudflare Workers, paired with an AI-assisted content pipeline&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; astro &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cloudflare&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;workers &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; blog&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.RealFakeJobPredictor &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Full-stack app with an ML core detecting fraudulent job postings - Python/ML backend, AWS CDK infrastructure, and frontend&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; machine&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learning &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; fraud&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;detection &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;cdk&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ImageProcessing.BackgroundReplace &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Computer vision app for removing/replacing image backgrounds (color-based and transparency methods) with a UI - CS 6322 grad term project&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; computer&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;vision &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; image&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;processing &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; python&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;RD.Creations.Site &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Order intake site and mini-CRM for RD Creations, a Valley Mills, TX bakery - Astro + Tailwind + Supabase, deployable to Cloudflare Pages or Vercel&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; astro &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; supabase &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; tailwindcss&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;RD.Creations.Site &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;visibility private&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# --- Descriptions + topics: Fix repos ---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Projects.Movies.ReviewSentimentAnalysis &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Full-stack movie review sentiment analysis app - Python backend, frontend, and AWS CDK infrastructure&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; sentiment&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;analysis &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;cdk &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; nlp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Datamining.Classification.TumorType &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Classification pipeline predicting tumor type from clinical data - preprocessing, model training, and visualization&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; classification &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; machine&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learning &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; scikit&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Datamining.Classification.CustomerSegmentation &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Classification pipeline segmenting customers by behavioral and demographic features&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; classification &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; machine&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learning&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;Datamining.Clustering.CreditCardDataset &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Clustering analysis on credit card transaction data to identify spending/behavior segments&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; clustering &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; unsupervised&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learning &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; machine&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;learning&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;rss&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;reader &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;AWS CDK (TypeScript) backend for an RSS aggregator - categorized feeds with on-demand AI summaries&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;cdk &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; typescript &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; serverless&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;rss&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;reader&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ui &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Vue 3 + Vite frontend for the AWS RSS Reader project (WIP, paired with aws-rss-reader backend)&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; vue &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; frontend&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;siteCrawler &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Java AWS Lambda function for crawling websites, with CI configured via GitHub Actions&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; aws&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;lambda &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; java &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; serverless&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;gh repo edit jdeleon3&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;rag&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;saas&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;mvp &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;description &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Production-ready RAG SaaS MVP - FastAPI + pgvector backend, React frontend, Ollama LLM inference, Docker Compose&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; rag &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; fastapi &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; llm &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;add-topic&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; docker&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/staged-script.png&quot; alt=&quot;Staging deletion scripts in VS Code terminal before execution&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Human review &amp;amp; CLI permissions&lt;/h2&gt;
&lt;p&gt;Generating the audit and scripts took roughly 20 minutes. But reviewing the &lt;code&gt;.ps1&lt;/code&gt; files in VS Code before hitting enter was essential because &lt;code&gt;gh repo delete&lt;/code&gt; doesn’t ask twice, and &lt;code&gt;delete_stale_repos.ps1&lt;/code&gt; had nine specific repo names on it that I needed to personally confirm before the terminal ever saw them. Generating CLI scripts takes seconds; running &lt;code&gt;gh repo delete&lt;/code&gt; without checking the target list first is how you delete a repository you actually needed.&lt;/p&gt;
&lt;p&gt;During the first run of &lt;code&gt;delete_stale_repos.ps1&lt;/code&gt;, the GitHub CLI came back with a 403 because the authenticated &lt;code&gt;gh&lt;/code&gt; session didn’t carry the OAuth scope needed to delete repositories.&lt;/p&gt;
&lt;p&gt;Standard &lt;code&gt;gh&lt;/code&gt; CLI credentials omit deletion privileges by default. Requesting the explicit &lt;code&gt;delete_repo&lt;/code&gt; OAuth scope resolved it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;gh&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; auth&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; refresh&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -h&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; github.com&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; delete_repo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once re-authenticated, executing &lt;code&gt;delete_stale_repos.ps1&lt;/code&gt; and &lt;code&gt;consolidate_stock_repos.ps1&lt;/code&gt; deleted all 11 targeted repositories in seconds:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/executed-deletion.png&quot; alt=&quot;Execution output showing batch deletion of stale repositories with green checkmarks&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The final result: Reconciling the count&lt;/h2&gt;
&lt;p&gt;Deleting 9 dead-weight repositories plus 2 redundant stock trading stubs removed 11 repositories total. The account dropped from 25 repositories down to exactly 14 active, clean projects.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/profile-after-pinned.png&quot; alt=&quot;GitHub profile after cleanup showing 14 clean repositories and pinned projects&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The repository list now features single-sentence descriptions and topic tags across all remaining codebases:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://makingcode.io/images/blog/auditing-25-github-repos-with-claude/repos-after-tags.png&quot; alt=&quot;Repositories list after updating metadata and adding topic tags&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;The audit and batch cleanup cleared out the profile noise, but a few follow-up tasks remain on the backlog:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Full README Write-Ups&lt;/strong&gt;: Draft documentation for &lt;code&gt;Projects.Movies.ReviewSentimentAnalysis&lt;/code&gt; and &lt;code&gt;aws-lambda-siteCrawler&lt;/code&gt;, which both contain substantial code but zero documentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;rag-saas-mvp&lt;/code&gt; Direction&lt;/strong&gt;: Decide whether to continue active development on the RAG SaaS MVP (FastAPI + pgvector + Docker) or publish it publicly with an explicit status note indicating it is a completed MVP snapshot.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coursework Consolidation&lt;/strong&gt;: Bundle the three separate Data Mining repositories (&lt;code&gt;Datamining.Classification.TumorType&lt;/code&gt;, &lt;code&gt;Datamining.Classification.CustomerSegmentation&lt;/code&gt;, &lt;code&gt;Datamining.Clustering.CreditCardDataset&lt;/code&gt;) into a single &lt;code&gt;ML-Coursework&lt;/code&gt; repository with a unified README summarizing each dataset and technique.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’ve run a similar cleanup pass on your own GitHub account, reply and tell me what your audit turned up.&lt;/p&gt;
</content:encoded><category>github</category><category>claude</category><category>ai-tools</category><category>workflow</category></item><item><title>Securing a Contact Pipeline: Astro, Cloudflare Pages Functions, Turnstile, and n8n</title><link>https://makingcode.io/blog/securing-a-contact-pipeline-cloudflare-turnstile-n8n/</link><guid isPermaLink="true">https://makingcode.io/blog/securing-a-contact-pipeline-cloudflare-turnstile-n8n/</guid><description>How I replaced hardcoded mailto links with an edge gateway that verifies humans, signs its own JWT, and hands off to a self-hosted n8n workflow — and the two local-dev gotchas that came with it.</description><pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A &lt;code&gt;mailto:&lt;/code&gt; link is not a contact pipeline — it’s a hope that the visitor’s OS has a mail client configured. Replacing it meant building something with an actual trust boundary: verify the submitter is human, authenticate the request before it touches internal infrastructure, and never let the browser talk to the automation server directly. Here’s how that pipeline is put together.&lt;/p&gt;
&lt;h2&gt;The shape of the problem&lt;/h2&gt;
&lt;p&gt;The site is static (Astro, &lt;code&gt;output: &apos;static&apos;&lt;/code&gt;), and the destination is a self-hosted n8n instance on a Hetzner VPC. Those two facts rule out the naive approach — a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; posting straight to the n8n webhook URL — for two reasons: the webhook URL becomes public the moment it’s in client-side JS, and there’s nothing stopping a script from hammering it with junk submissions.&lt;/p&gt;
&lt;p&gt;The fix is a broker in the middle: a Cloudflare Pages Function sitting at the edge, doing three things in strict order before anything reaches n8n.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Browser (Turnstile widget + form)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → POST /api/submit-contact          (Cloudflare Pages Function)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      1. verify Turnstile token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      2. sign a short-lived JWT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      3. forward payload + JWT to n8n&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → n8n Webhook node (Hetzner VPC)     (JWT Auth credential checks the signature)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser never sees the n8n URL, never sees a credential, and can’t reach the webhook without first clearing a human check.&lt;/p&gt;
&lt;h2&gt;Turnstile as the gate, not the lock&lt;/h2&gt;
&lt;p&gt;The form embeds Cloudflare Turnstile as a widget scoped to the &lt;code&gt;ContactForm.astro&lt;/code&gt; component rather than loaded globally, so pages without the form don’t pay for it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;cf-turnstile&quot;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; data-sitekey&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;{turnstileSiteKey}&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; data-theme&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;dark&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; src&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;https://challenges.cloudflare.com/turnstile/v0/api.js&quot;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; defer&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;script&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the widget lives inside a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, Turnstile auto-injects its response token into a hidden &lt;code&gt;cf-turnstile-response&lt;/code&gt; field — no manual callback wiring needed. The client-side script just reads it out of &lt;code&gt;FormData&lt;/code&gt; on submit and ships it alongside the form fields as JSON.&lt;/p&gt;
&lt;p&gt;The verification itself happens server-side, in the Pages Function, against Cloudflare’s &lt;code&gt;siteverify&lt;/code&gt; endpoint:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; verifyTurnstile&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;token&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;secret&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;remoteIp&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;boolean&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; body&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; URLSearchParams&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({ secret, response: token });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (remoteIp) body.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;remoteip&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, remoteIp);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;https://challenges.cloudflare.com/turnstile/v0/siteverify&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    method: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    headers: { &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;content-type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;application/x-www-form-urlencoded&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    body,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;response.ok) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; response.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;()) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;success&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; result.success &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A client-side token is a hint, not proof — trusting the widget without this round trip would mean trusting whatever JavaScript happened to run in the visitor’s browser.&lt;/p&gt;
&lt;h2&gt;From OAuth to a self-signed JWT&lt;/h2&gt;
&lt;p&gt;The first cut of this pipeline authenticated to n8n with an OAuth 2.0 client-credentials grant: the Pages Function would exchange a client ID and secret at a token endpoint for a bearer token, then attach that token to the n8n request. It worked, but it bought more than the problem needed — a second network hop, a second point of failure, and an external token endpoint to keep alive, all to protect a single internal webhook that only one caller (this function) ever hits.&lt;/p&gt;
&lt;p&gt;I replaced it with a self-signed HS256 JWT, minted inline using the Workers-native &lt;code&gt;SubtleCrypto&lt;/code&gt; API — no token endpoint, no extra dependency, no extra round trip:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; signContactJwt&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;secret&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;&amp;gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; now&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;floor&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(Date.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;now&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1000&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; header&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { alg: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;HS256&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, typ: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;JWT&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    iss: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;makingcode-contact-form&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    sub: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;contact-submission&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    iat: now,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    exp: now &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 60&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// single-use, short-lived&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; signingInput&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; `${&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;base64UrlEncode&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;header&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;))&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;}.${&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;base64UrlEncode&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;))&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; key&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; crypto.subtle.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;importKey&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;    &apos;raw&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; TextEncoder&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;encode&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(secret),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    { name: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;HMAC&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, hash: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;SHA-256&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    false&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    [&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;sign&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; signature&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; crypto.subtle.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;sign&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;HMAC&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, key, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; TextEncoder&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;encode&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(signingInput));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; `${&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;signingInput&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;}.${&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;base64UrlEncode&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;signature&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;n8n’s Webhook node verifies it on the way in with its built-in &lt;strong&gt;JWT Auth&lt;/strong&gt; credential, checked against the same shared secret. The token is deliberately short-lived (60 seconds) and single-purpose — it exists only to prove “this request came from the contact function,” not to carry a session or identity. That’s a better fit for a single machine-to-machine hop than a full OAuth grant, and it removes an entire external dependency from the request path.&lt;/p&gt;
&lt;h2&gt;Isolating two type universes&lt;/h2&gt;
&lt;p&gt;Adding &lt;code&gt;@cloudflare/workers-types&lt;/code&gt; to get proper typing for the Pages Function (&lt;code&gt;PagesFunction&amp;lt;Env&amp;gt;&lt;/code&gt;, the &lt;code&gt;crypto.subtle&lt;/code&gt; global, etc.) initially broke an unrelated file. The cause: a &lt;code&gt;/// &amp;lt;reference types=&quot;@cloudflare/workers-types&quot; /&amp;gt;&lt;/code&gt; directive doesn’t scope itself to the file it’s written in — it pulls the whole package into the &lt;em&gt;global&lt;/em&gt; TypeScript program. Workers-types declares its own global &lt;code&gt;Element&lt;/code&gt; interface (for &lt;code&gt;HTMLRewriter&lt;/code&gt;), and its &lt;code&gt;append()&lt;/code&gt; signature collided with the DOM lib’s &lt;code&gt;Element.append()&lt;/code&gt;, breaking type-checking in a component that had nothing to do with the contact form.&lt;/p&gt;
&lt;p&gt;The fix was to stop merging the two globally. &lt;code&gt;functions/&lt;/code&gt; got its own &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;compilerOptions&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;target&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;ES2022&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;module&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;ES2022&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;moduleResolution&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;Bundler&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;lib&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;ES2022&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;types&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;@cloudflare/workers-types&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;strict&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;skipLibCheck&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    &quot;noEmit&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  &quot;include&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;**/*.ts&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…excluded from the root &lt;code&gt;tsconfig.json&lt;/code&gt; (&lt;code&gt;&quot;exclude&quot;: [&quot;dist&quot;, &quot;functions&quot;]&lt;/code&gt;), and checked as a separate step in the build:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;astro check &amp;amp;&amp;amp; tsc --noEmit -p functions/tsconfig.json &amp;amp;&amp;amp; astro build&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astro’s browser-facing code keeps the DOM lib it needs; the edge function keeps the Workers runtime types it needs. Neither program sees the other’s globals.&lt;/p&gt;
&lt;h2&gt;The local-dev trap: &lt;code&gt;astro dev&lt;/code&gt; doesn’t run Pages Functions&lt;/h2&gt;
&lt;p&gt;The first local test of the finished form failed with a 404 on &lt;code&gt;/api/submit-contact&lt;/code&gt; — not a bug in the function, but a gap in how it was being served. &lt;code&gt;astro dev&lt;/code&gt; is a plain Vite dev server; it only knows about routes under &lt;code&gt;src/pages/&lt;/code&gt;. It has no concept of a &lt;code&gt;functions/&lt;/code&gt; directory, so any request to &lt;code&gt;/api/*&lt;/code&gt; simply has nowhere to go.&lt;/p&gt;
&lt;p&gt;Cloudflare Pages Functions only run under Wrangler’s Pages emulator (or in production, on Cloudflare itself). The fix was a dedicated script that builds the static site and serves it through that emulator instead:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;pages:dev&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;astro build &amp;amp;&amp;amp; wrangler pages dev dist --env-file .env --compatibility-date=2026-07-07&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wrangler pages dev&lt;/code&gt; reads secrets straight from &lt;code&gt;.env&lt;/code&gt;, wires up &lt;code&gt;functions/&lt;/code&gt; alongside the static output, and serves both from one local server — the same shape the request will take in production. Plain &lt;code&gt;npm run dev&lt;/code&gt; is still the right tool for fast UI iteration; it just can’t be used to test the submission path itself.&lt;/p&gt;
&lt;h2&gt;What’s left on n8n’s side&lt;/h2&gt;
&lt;p&gt;Everything up to the webhook is done and verified locally: Turnstile blocks non-human submissions, the Pages Function signs a short-lived JWT per request, and the request reaches n8n with that token in the &lt;code&gt;Authorization&lt;/code&gt; header. The remaining piece is entirely on the n8n workflow — the Webhook node’s JWT Auth credential needs the same HS256 secret configured, and whatever runs after it (CRM write, notification, whatever the workflow does with a new lead) is a normal n8n build from there.&lt;/p&gt;
</content:encoded><category>architecture</category><category>automation</category><category>security</category></item><item><title>Architecting Idempotent Webhook Pipelines</title><link>https://makingcode.io/blog/architecting-idempotent-webhook-pipelines/</link><guid isPermaLink="true">https://makingcode.io/blog/architecting-idempotent-webhook-pipelines/</guid><description>Why &quot;at-least-once&quot; delivery breaks naive CRM integrations, and the idempotency-key pattern that fixes it for good.</description><pubDate>Mon, 18 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most CRM and payment providers guarantee &lt;strong&gt;at-least-once&lt;/strong&gt; webhook delivery, not exactly-once. If your system assumes every webhook arrives exactly one time, you will eventually double-book an appointment, double-charge a client, or double-notify an agent. This is the architectural trade-off at the center of every serious automation build: correctness under retries costs a little extra state, but the alternative costs trust.&lt;/p&gt;
&lt;h2&gt;The trade-off&lt;/h2&gt;
&lt;p&gt;There are three common strategies, in increasing order of robustness and cost:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Do nothing&lt;/strong&gt; — cheapest, breaks in production the first time a provider retries after a slow 200.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timestamp/window de-duplication&lt;/strong&gt; — cheap, but fragile around clock skew and burst retries.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency-key ledger&lt;/strong&gt; — a small persisted table keyed on the provider’s event ID, checked before any side effect runs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For anything touching money, scheduling, or client communication, option 3 is the only defensible choice.&lt;/p&gt;
&lt;h2&gt;Implementation: the idempotency-key middleware&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { Request, Response, NextFunction } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;express&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { Pool } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;pg&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; WebhookEvent&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  id&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;  payload&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; unknown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; idempotencyGuard&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;pool&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; Pool&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; Request&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;next&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; NextFunction&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; event&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; req.body &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; WebhookEvent&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;rowCount&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; pool.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;query&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;      `INSERT INTO webhook_events (event_id, event_type, received_at)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;       VALUES ($1, $2, now())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;       ON CONFLICT (event_id) DO NOTHING`&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      [event.id, event.type],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (rowCount &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;      // Already processed this exact event ID — ack without re-running side effects.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      res.&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;({ status: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&apos;duplicate_ignored&apos;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;    next&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ON CONFLICT DO NOTHING&lt;/code&gt; clause is doing the real work here: the uniqueness constraint on &lt;code&gt;event_id&lt;/code&gt; turns a race between two concurrent retries into a no-op for the loser, with no explicit locking required.&lt;/p&gt;
&lt;h2&gt;Queueing the guarded events&lt;/h2&gt;
&lt;p&gt;Once an event passes the guard, it’s handed to a durable queue rather than processed inline — so a downstream failure (CRM API timeout, rate limit) doesn’t force the webhook endpoint itself to block or fail:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# consumer-config.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;queue&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;webhook-events&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  visibility_timeout_seconds&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;30&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  max_receive_count&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  dead_letter_queue&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;webhook-events-dlq&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;consumer&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  concurrency&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;  backoff&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    strategy&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;exponential&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    base_ms&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7EE787&quot;&gt;    max_ms&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;30000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Verifying the fix&lt;/h2&gt;
&lt;p&gt;Replaying a captured webhook payload against a local instance confirms the guard behaves correctly under a simulated retry:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3;overflow-x:auto;white-space:pre-wrap;word-wrap:break-word&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# First delivery — should process normally&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -X&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; http://localhost:8787/webhooks/crm&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  -H&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  -d&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;{&quot;id&quot;:&quot;evt_9f8a1c&quot;,&quot;type&quot;:&quot;lead.created&quot;,&quot;payload&quot;:{}}&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# Simulated retry of the same event — should be ignored, not reprocessed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; -X&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; http://localhost:8787/webhooks/crm&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  -H&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  -d&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; &apos;{&quot;id&quot;:&quot;evt_9f8a1c&quot;,&quot;type&quot;:&quot;lead.created&quot;,&quot;payload&quot;:{}}&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the second call returns &lt;code&gt;duplicate_ignored&lt;/code&gt; and no second side effect fires, the ledger is doing its job. This same pattern — a small keyed table in front of anything with a side effect — is the one I reach for by default now, regardless of which CRM or messaging provider sits on the other end.&lt;/p&gt;
</content:encoded><category>architecture</category><category>automation</category><category>reliability</category></item></channel></rss>