DEV Community

Abir Joshi
Abir Joshi

Posted on

API Middleware – a self-hosted API gateway with DLP scanning built in Laravel 11

got tired of paying for managed API gateways that didn't let me inspect the data flowing through them. So I built API Middleware — open source, self-hosted, one command to run.

GitHub: https://github.com/joshiabir/theapimiddleware

What it does

API Middleware sits between your clients and your backend services. Instead of clients hitting your backend directly, they hit the gateway, which:

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

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

Architecture

Two Laravel 11 apps sharing one MySQL database:

fiona — the gateway

Request → policyControl middleware → DLP scan → HTTP proxy → DLP scan → Response
                                                                ↓
                                                         Queue job (logging)
Enter fullscreen mode Exit fullscreen mode

The middleware chain in policyControl:

  1. Looks up the domain in domain_routings
  2. Finds the cluster and API config for the URL
  3. Applies policies: rate limits, auth, IP filtering, honeypot detection
  4. If all checks pass, forwards the request

The proxy uses Laravel's Http::send() 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.

plucker-app — the admin dashboard

Built with Livewire v3 and Jetstream. 54 Livewire components cover:

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

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.

The DLP engine

Each domain can have multiple DLP policies. The scanner chains them all against the request or response body:

// Keyword match
$redacted_body = preg_replace('/' . preg_quote($policy->value, '/') . '/', '[redacted]', $body);

// Regex pattern match  
$pattern = '~' . $policy->value . '~';
$redacted_body = preg_replace($pattern, '[redacted]', $body);
Enter fullscreen mode Exit fullscreen mode

Actions per policy:

  • alert — log the match, pass the original body through
  • redact — replace matches with [redacted] in the forwarded body
  • block — return 403 if any match is found

Invalid regex patterns are caught and skipped gracefully so one bad rule doesn't break everything.

Setup

git clone https://github.com/joshiabir/theapimiddleware.git
cd theapimiddleware
./setup.sh
Enter fullscreen mode Exit fullscreen mode

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 .env, and starts all containers.

Two services come up:

  • Gateway (fiona): http://localhost:8000
  • Admin dashboard (plucker-app): http://localhost:8001

Testing

Full PestPHP test suite using SQLite in-memory — no database needed to run tests.

cd fiona && ./vendor/bin/pest
cd plucker-app && ./vendor/bin/pest
Enter fullscreen mode Exit fullscreen mode

Covers: DLP service logic, policy middleware enforcement, proxy flow, Livewire dashboard components.


Open to feedback on the architecture, especially the shared-DB pattern and the DLP chaining approach. What would you do differently?

Top comments (0)