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)
The middleware chain in policyControl:
- Looks up the domain in
domain_routings - Finds the cluster and API config for the URL
- Applies policies: rate limits, auth, IP filtering, honeypot detection
- 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);
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
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
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)