<?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: Abir Joshi</title>
    <description>The latest articles on DEV Community by Abir Joshi (@joshiabir).</description>
    <link>https://dev.to/joshiabir</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2208384%2F0e0ab6c3-1469-42bb-8085-cfa689c17025.jpg</url>
      <title>DEV Community: Abir Joshi</title>
      <link>https://dev.to/joshiabir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joshiabir"/>
    <language>en</language>
    <item>
      <title>API Middleware – a self-hosted API gateway with DLP scanning built in Laravel 11</title>
      <dc:creator>Abir Joshi</dc:creator>
      <pubDate>Thu, 02 Apr 2026 13:12:48 +0000</pubDate>
      <link>https://dev.to/joshiabir/api-middleware-a-self-hosted-api-gateway-with-dlp-scanning-built-in-laravel-11-2g25</link>
      <guid>https://dev.to/joshiabir/api-middleware-a-self-hosted-api-gateway-with-dlp-scanning-built-in-laravel-11-2g25</guid>
      <description>&lt;p&gt;got tired of paying for managed API gateways that didn't let me inspect the data flowing through them. So I built &lt;strong&gt;API Middleware&lt;/strong&gt; — open source, self-hosted, one command to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub: &lt;a href="https://github.com/joshiabir/theapimiddleware" rel="noopener noreferrer"&gt;https://github.com/joshiabir/theapimiddleware&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;API Middleware sits between your clients and your backend services. Instead of clients hitting your backend directly, they hit the gateway, which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes traffic based on the incoming domain&lt;/li&gt;
&lt;li&gt;Scans request and response bodies for sensitive data (DLP)&lt;/li&gt;
&lt;li&gt;Enforces rate limits (global and per-user)&lt;/li&gt;
&lt;li&gt;Checks authentication before forwarding&lt;/li&gt;
&lt;li&gt;Logs every request with timing — asynchronously, so it never slows down the client&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is configured through a web admin dashboard. No config files, no redeployments — change a DLP rule and it's live in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Two Laravel 11 apps sharing one MySQL database:&lt;/p&gt;

&lt;h3&gt;
  
  
  fiona — the gateway
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → policyControl middleware → DLP scan → HTTP proxy → DLP scan → Response
                                                                ↓
                                                         Queue job (logging)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware chain in &lt;code&gt;policyControl&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Looks up the domain in &lt;code&gt;domain_routings&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Finds the cluster and API config for the URL&lt;/li&gt;
&lt;li&gt;Applies policies: rate limits, auth, IP filtering, honeypot detection&lt;/li&gt;
&lt;li&gt;If all checks pass, forwards the request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The proxy uses Laravel's &lt;code&gt;Http::send()&lt;/code&gt; with the original headers and method intact. After the response comes back, DLP runs again on the response body. Log writes go to a queue job so they never add latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  plucker-app — the admin dashboard
&lt;/h3&gt;

&lt;p&gt;Built with Livewire v3 and Jetstream. 54 Livewire components cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain routing management&lt;/li&gt;
&lt;li&gt;Cluster and API configuration&lt;/li&gt;
&lt;li&gt;DLP rule editor (keywords, regex patterns, bypass URLs)&lt;/li&gt;
&lt;li&gt;Request log viewer with filtering and CSV export&lt;/li&gt;
&lt;li&gt;Security incident feed&lt;/li&gt;
&lt;li&gt;OAuth login (Google, GitHub, Microsoft)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database-driven config is the key design decision: fiona reads policy state from the DB on every request, plucker writes to it. No signal needed between the two apps — policy changes are instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DLP engine
&lt;/h2&gt;

&lt;p&gt;Each domain can have multiple DLP policies. The scanner chains them all against the request or response body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Keyword match&lt;/span&gt;
&lt;span class="nv"&gt;$redacted_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;preg_quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[redacted]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Regex pattern match  &lt;/span&gt;
&lt;span class="nv"&gt;$pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'~'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$policy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'~'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$redacted_body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[redacted]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actions per policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;alert&lt;/code&gt; — log the match, pass the original body through&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redact&lt;/code&gt; — replace matches with &lt;code&gt;[redacted]&lt;/code&gt; in the forwarded body&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;block&lt;/code&gt; — return 403 if any match is found&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Invalid regex patterns are caught and skipped gracefully so one bad rule doesn't break everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/joshiabir/theapimiddleware.git
&lt;span class="nb"&gt;cd &lt;/span&gt;theapimiddleware
./setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The setup script installs Docker if it's not present (macOS via Homebrew, Ubuntu/Debian/Fedora via package manager), generates app keys and DB passwords, writes &lt;code&gt;.env&lt;/code&gt;, and starts all containers.&lt;/p&gt;

&lt;p&gt;Two services come up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gateway (fiona): &lt;code&gt;http://localhost:8000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Admin dashboard (plucker-app): &lt;code&gt;http://localhost:8001&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Full PestPHP test suite using SQLite in-memory — no database needed to run tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;fiona &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./vendor/bin/pest
&lt;span class="nb"&gt;cd &lt;/span&gt;plucker-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./vendor/bin/pest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Covers: DLP service logic, policy middleware enforcement, proxy flow, Livewire dashboard components.&lt;/p&gt;




&lt;p&gt;Open to feedback on the architecture, especially the shared-DB pattern and the DLP chaining approach. What would you do differently?&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>docker</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
