<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Piyush Anand</title>
    <description>The latest articles on DEV Community by Piyush Anand (@creatorpiyush).</description>
    <link>https://dev.to/creatorpiyush</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F738407%2F2ee69e43-d81e-4ced-b744-d1223a7cc039.jpeg</url>
      <title>DEV Community: Piyush Anand</title>
      <link>https://dev.to/creatorpiyush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/creatorpiyush"/>
    <language>en</language>
    <item>
      <title>Stop your Flutter architecture from drifting: layer rules and cycle checks in your IDE and CI with arch_guard</title>
      <dc:creator>Piyush Anand</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:27:15 +0000</pubDate>
      <link>https://dev.to/creatorpiyush/stop-your-flutter-architecture-from-drifting-layer-rules-and-cycle-checks-in-your-ide-and-ci-with-46nd</link>
      <guid>https://dev.to/creatorpiyush/stop-your-flutter-architecture-from-drifting-layer-rules-and-cycle-checks-in-your-ide-and-ci-with-46nd</guid>
      <description>&lt;p&gt;Every Flutter project I've worked on started with a clean folder structure: &lt;code&gt;domain&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;presentation&lt;/code&gt;, maybe a &lt;code&gt;features/&lt;/code&gt; folder. And on every one, six months later, a widget was importing a repository implementation directly, and two services imported each other.&lt;/p&gt;

&lt;p&gt;Nobody did it on purpose. A deadline, a quick fix, a review that missed one import line. The analyzer doesn't care, the tests pass, and the architecture diagram in the wiki is now fiction.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;arch_guard&lt;/strong&gt;: a CLI, Dart library and analyzer plugin that reads every &lt;code&gt;import&lt;/code&gt;, &lt;code&gt;export&lt;/code&gt; and &lt;code&gt;part&lt;/code&gt; in your project and tells you when the code no longer matches the architecture you designed: in the editor as you type, in &lt;code&gt;dart test&lt;/code&gt;, and in CI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fus8x8wqo56u0860u7zau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fus8x8wqo56u0860u7zau.png" alt="Interactive dependency graph" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub add &lt;span class="nt"&gt;--dev&lt;/span&gt; arch_guard
dart run arch_guard init     &lt;span class="c"&gt;# detects your layout and writes arch_guard.yaml&lt;/span&gt;
dart run arch_guard          &lt;span class="c"&gt;# checks the project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; looks at your folders and picks a preset: Clean Architecture, Riverpod, Bloc or feature-first. It then previews the first scan: how many files landed in each layer, what's already broken, and what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Layer rules
&lt;/h3&gt;

&lt;p&gt;The simplest config is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# arch_guard.yaml&lt;/span&gt;
&lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clean_architecture&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means &lt;code&gt;domain&lt;/code&gt; may only import &lt;code&gt;core&lt;/code&gt;, and &lt;code&gt;presentation&lt;/code&gt; may not import &lt;code&gt;data&lt;/code&gt;. When something breaks a rule, you get the file, the line, and the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Layer Violation: [presentation] lib/features/auth/presentation/page.dart:3 -&amp;gt; [data] lib/features/auth/data/repo.dart
   Rule: `presentation` may only import `core`, `domain`, `presentation`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other presets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;riverpod&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;domain&lt;/code&gt; imports nothing else; &lt;code&gt;data&lt;/code&gt; never imports &lt;code&gt;application&lt;/code&gt; or &lt;code&gt;presentation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;bloc&lt;/code&gt;&lt;/strong&gt;: UI → bloc/cubit → repository → data provider, with &lt;code&gt;models&lt;/code&gt; usable everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;feature_first&lt;/code&gt;&lt;/strong&gt;: each folder in &lt;code&gt;lib/features&lt;/code&gt; becomes its own layer, and a feature may import only itself and shared code. This is the one I find catches the most: features quietly reaching into each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Presets are a starting point. Override just the part you disagree with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clean_architecture&lt;/span&gt;
&lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;presentation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;allowed_imports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;core&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;presentation&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# we allow UI -&amp;gt; data here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Forbidden packages
&lt;/h3&gt;

&lt;p&gt;Some rules aren't about your own folders. "The domain layer must not know about Flutter" is a package rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;forbidden_packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dio&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;            &lt;span class="c1"&gt;# nowhere in the project&lt;/span&gt;
&lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;forbidden_packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;flutter&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dart:io"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firebase_*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Circular dependencies, with a fix
&lt;/h3&gt;

&lt;p&gt;arch_guard runs Tarjan's algorithm over the import graph and reports every group of files that import each other, with fan-in/fan-out, instability and the "hub" files. Each cycle also names the one import to remove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Strongly Connected Component #1 (2 files)
  Representative Cycle Path:
  lib/widgets/nav.dart
  -&amp;gt; lib/screens/home.dart
  -&amp;gt; lib/widgets/nav.dart
  Suggested fix: remove lib/screens/home.dart:1 -&amp;gt; lib/widgets/nav.dart (breaks the cycle)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;max_scc_size: 5&lt;/code&gt; fails the run if a cycle group grows beyond 5 files, and &lt;code&gt;dart run arch_guard --explain &amp;lt;file&amp;gt;&lt;/code&gt; shows why a file is in one.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Coupling limits and monorepos
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;metrics: { max_fan_out: 20, max_fan_in: 30 }&lt;/code&gt; stops files from turning into hubs; reports list the most coupled files.&lt;/li&gt;
&lt;li&gt;In a Dart workspace or Melos-style &lt;code&gt;packages/&lt;/code&gt; + &lt;code&gt;apps/&lt;/code&gt; repo, &lt;code&gt;workspace_rules&lt;/code&gt; say which packages may depend on which (&lt;code&gt;apps/*&lt;/code&gt; → &lt;code&gt;packages/*&lt;/code&gt;, never the reverse), and cycles across package boundaries are found too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. A graph you can actually look at
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart run arch_guard &lt;span class="nt"&gt;-f&lt;/span&gt; html &lt;span class="nt"&gt;-o&lt;/span&gt; build/reports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gives you an interactive graph that works offline. There's also Mermaid (paste it into a PR or README), Graphviz DOT and JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  In your editor
&lt;/h2&gt;

&lt;p&gt;CI catching a bad import is good; never writing it is better. The companion&lt;br&gt;
&lt;strong&gt;arch_guard_lint&lt;/strong&gt; analyzer plugin reads the same &lt;code&gt;arch_guard.yaml&lt;/code&gt; and underlines the import in VS Code, Android Studio and &lt;code&gt;dart analyze&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# analysis_options.yaml (Dart 3.10 / Flutter 3.38+)&lt;/span&gt;
&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;arch_guard_lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^0.2.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[IDE clip: underlined import → quick fix]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;layer_violation&lt;/code&gt; and &lt;code&gt;forbidden_package&lt;/code&gt; on the offending import.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;circular_dependency&lt;/code&gt; on the one import that closes each cycle, updated as you type, including unsaved changes.&lt;/li&gt;
&lt;li&gt;Quick fixes: &lt;strong&gt;Remove this import&lt;/strong&gt;, or &lt;strong&gt;Allow layer X to import Y&lt;/strong&gt;, which edits &lt;code&gt;allowed_imports&lt;/code&gt; in your config for you. There's deliberately no "allow" fix for forbidden packages: loosening a "never" rule should be a conscious edit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  In your tests
&lt;/h2&gt;

&lt;p&gt;If you'd rather keep architecture rules next to the code, they also run in &lt;code&gt;dart test&lt;/code&gt; / &lt;code&gt;flutter test&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:arch_guard/testing.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:test/test.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'architecture'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;expectArchitecture&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails exactly when the CLI would, with a readable list of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adopting it on a codebase that's already messy
&lt;/h2&gt;

&lt;p&gt;This was the part I cared most about. If a tool reports 200 violations on day one, it gets uninstalled on day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart run arch_guard &lt;span class="nt"&gt;--update-baseline&lt;/span&gt;
git add arch_guard_baseline.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That records today's problems. From then on, CI fails only on &lt;strong&gt;new&lt;/strong&gt; cycles and violations (the IDE plugin stays quiet about baselined ones too). When someone fixes an old one, arch_guard tells you, so you can tighten the baseline and stop it from coming back.&lt;/p&gt;

&lt;p&gt;For big PRs on big apps, &lt;code&gt;--since origin/main&lt;/code&gt; reports only problems touching files changed on the branch, while still analysing the whole project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on every pull request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/architecture.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Architecture&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;arch_guard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;creatorpiyush/arch_guard@v1.8.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action downloads a checksum-verified binary (no Dart SDK setup), keeps a single PR comment up to date, and can upload SARIF so violations show up in GitHub Code Scanning. There's also a pre-commit hook and recipes for lefthook, plain git hooks and GitLab CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DCM&lt;/strong&gt; is a full linting suite with hundreds of rules and IDE integration; its free tier covers one seat up to 50k LOC. If you already use it, its banned-imports rule covers part of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;import_lint&lt;/strong&gt; shows import rules right in your editor as an analyzer plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lakos&lt;/strong&gt; focuses on graph metrics and DOT/JSON output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;arch_guard does one job: make architecture rules easy to adopt (presets, &lt;code&gt;init&lt;/code&gt;, baselines) and enforce them everywhere you work: editor, tests and every PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 pub.dev: &lt;a href="https://pub.dev/packages/arch_guard" rel="noopener noreferrer"&gt;https://pub.dev/packages/arch_guard&lt;/a&gt; and &lt;a href="https://pub.dev/packages/arch_guard_lint" rel="noopener noreferrer"&gt;https://pub.dev/packages/arch_guard_lint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 Docs (a recipe per architecture): &lt;a href="https://creatorpiyush.github.io/arch_guard/" rel="noopener noreferrer"&gt;https://creatorpiyush.github.io/arch_guard/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧪 Examples (one small project per feature, with real output): &lt;a href="https://github.com/creatorpiyush/arch_guard/blob/main/example/example.md" rel="noopener noreferrer"&gt;https://github.com/creatorpiyush/arch_guard/blob/main/example/example.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐ GitHub: &lt;a href="https://github.com/creatorpiyush/arch_guard" rel="noopener noreferrer"&gt;https://github.com/creatorpiyush/arch_guard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT-licensed. If your project uses a layout none of the presets fit, I'd really like to hear about it: open an issue or leave a comment with your folder structure, and I'll look at adding a preset.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why your webhook signature check fails (and the bugs that pass it)</title>
      <dc:creator>Piyush Anand</dc:creator>
      <pubDate>Sat, 03 Oct 2026 13:07:05 +0000</pubDate>
      <link>https://dev.to/creatorpiyush/why-your-webhook-signature-check-fails-and-the-bugs-that-pass-it-10fo</link>
      <guid>https://dev.to/creatorpiyush/why-your-webhook-signature-check-fails-and-the-bugs-that-pass-it-10fo</guid>
      <description>&lt;p&gt;In July I wrote about &lt;a href="https://medium.com/@creatorpiyush/i-got-tired-of-copy-pasting-hmac-code-for-every-webhook-provider-so-i-built-verihook-f6355f4c314d" rel="noopener noreferrer"&gt;why I built verihook&lt;/a&gt;: every provider signs webhooks differently, and I was tired of maintaining five slightly different HMAC functions. Since then &lt;a href="https://github.com/creatorpiyush/verihook" rel="noopener noreferrer"&gt;verihook&lt;/a&gt; has grown to 40+ providers, adapters for ten frameworks, testing helpers and a &lt;a href="https://creatorpiyush.github.io/verihook/" rel="noopener noreferrer"&gt;docs site&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Supporting that many providers taught me where webhook verification actually goes wrong. It's rarely the HMAC. It's everything around it: the body, the secret, the URL, retries and tests. Here are the five mistakes I see most, including the worse ones that make verification &lt;em&gt;pass&lt;/em&gt; when it shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The body parser ate your signature
&lt;/h2&gt;

&lt;p&gt;This is the most common failure by far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhooks/stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// not the bytes Stripe signed&lt;/span&gt;
  &lt;span class="c1"&gt;// signature check fails with the right secret&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider signed the exact bytes it sent. &lt;code&gt;JSON.stringify(JSON.parse(body))&lt;/code&gt; changes whitespace, escapes (&lt;code&gt;é&lt;/code&gt; vs &lt;code&gt;é&lt;/code&gt;) and number formatting. The fix is to verify the &lt;strong&gt;raw&lt;/strong&gt; body before any parser runs: &lt;code&gt;express.raw()&lt;/code&gt;, &lt;code&gt;await request.text()&lt;/code&gt;, Fastify's &lt;code&gt;rawBody&lt;/code&gt;, NestJS's &lt;code&gt;rawBody: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;verihook spots this case. When the body's size doesn't match &lt;code&gt;content-length&lt;/code&gt;, the result says so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// result.code: "INVALID_SIGNATURE"&lt;/span&gt;
&lt;span class="c1"&gt;// result.hint: "The body is 412 bytes but content-length is 431: it was modified&lt;/span&gt;
&lt;span class="c1"&gt;//   before verification, usually parsed as JSON and re-serialized. ..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The wrong secret (that looks right)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Stripe API key (&lt;code&gt;sk_...&lt;/code&gt;) instead of the endpoint's signing secret (&lt;code&gt;whsec_...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The test-mode secret in production.&lt;/li&gt;
&lt;li&gt;A Slack bot token instead of the signing secret.&lt;/li&gt;
&lt;li&gt;A trailing newline or quotes from the &lt;code&gt;.env&lt;/code&gt; file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four produce "signature mismatch" with no other clue. verihook recognizes the API key, the whitespace and the quotes from the secret's shape, and its hint names the mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The proxy changed the URL
&lt;/h2&gt;

&lt;p&gt;Twilio, Square and HubSpot sign the public URL they called. Behind ngrok, a load balancer or API Gateway, your server sees &lt;code&gt;http://10.0.0.5:3000/...&lt;/code&gt; instead of &lt;code&gt;https://api.example.com/...&lt;/code&gt;, and verification fails. Rebuild the URL from &lt;code&gt;x-forwarded-proto&lt;/code&gt; and &lt;code&gt;x-forwarded-host&lt;/code&gt;, or pass the public URL explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verification passes, but you processed the event twice
&lt;/h2&gt;

&lt;p&gt;Providers deliver &lt;em&gt;at least once&lt;/em&gt;. A timeout makes them retry, and a valid signature accepts every copy. You need deduplication, and the key matters: if you key it on a header the signature doesn't cover (GitHub's &lt;code&gt;x-github-delivery&lt;/code&gt;, say), an attacker can replay a captured webhook with a fresh header and walk past your dedupe store. verihook only keys on data the signature covers: a signed ID header, an ID inside the signed body, or a hash of the body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dedupeStore&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DUPLICATE_EVENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// stop the retries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Your tests pass because they test themselves
&lt;/h2&gt;

&lt;p&gt;This one bit me. verihook's Paddle verifier read &lt;code&gt;h=&lt;/code&gt; from the &lt;code&gt;Paddle-Signature&lt;/code&gt; header. Its test signer also wrote &lt;code&gt;h=&lt;/code&gt;. Every test passed. Paddle actually sends &lt;code&gt;h1=&lt;/code&gt;, so every real Paddle webhook was rejected.&lt;/p&gt;

&lt;p&gt;A test that signs with your code and verifies with your code proves the two agree, not that either matches the provider. What helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Known-good vectors&lt;/strong&gt; from the provider's docs: a payload, secret and signature you didn't compute yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conformance tests against official SDKs.&lt;/strong&gt; verihook's CI signs with the official Stripe, Octokit, Svix and Twilio SDKs and verifies with verihook, and the other way around.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Next.js App Router&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createWebhookHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verihook/next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createWebhookHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stripe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout.session.completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// result.event is typed&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are one-line adapters for Express, Fastify, Hono, NestJS, Nuxt, SvelteKit, Remix, Astro and AWS Lambda. It runs on Node, Deno, Bun and edge runtimes, and importing one provider (&lt;code&gt;verihook/stripe&lt;/code&gt;) costs about 4 kB. For tests, &lt;code&gt;signWebhook()&lt;/code&gt; builds signed requests for every provider, and &lt;code&gt;npx verihook simulate stripe&lt;/code&gt; sends one to your local server.&lt;/p&gt;

&lt;p&gt;The docs have a page per provider with where to find the secret in each dashboard: &lt;a href="https://creatorpiyush.github.io/verihook/" rel="noopener noreferrer"&gt;creatorpiyush.github.io/verihook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If a provider you use is missing, or a signature scheme looks wrong, I'd like to hear about it. &lt;a href="https://github.com/creatorpiyush/verihook/issues" rel="noopener noreferrer"&gt;Issues&lt;/a&gt; are open.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
      <category>security</category>
    </item>
  </channel>
</rss>
