Contributed — built the GHL Reports public API, Notifications and WebUtility modules
Agency Framework
A Dockerized microservices suite of a dozen tools — GHL Reports, WebUtility, Notifications, SecretShop, Activity Logs and more — where every service is its own app with its own database but shares one Core: authentication and the user/account model live in Core, and every tool loads under it. An agency signs in once and every service is multi-tenant-scoped by agency id.
01The problem
Consolidated, cost-safe reporting across many GoHighLevel sub-accounts whose analytics live in Cube.js over BigQuery, where each query costs real money — then exposing those numbers to external partners programmatically without hammering BigQuery or leaking one agency's data to another.
02The constraint
Every partner is a GHL location scoped to one agency, so tenant isolation must be airtight; BigQuery scans cost money so they can't run unbounded or per-request; and the public API had to reuse the existing internal metric engine — with all its per-agency quirks — rather than fork a parallel pipeline. One engineer, MVP-fast.
03What I built
Every tool runs as its own service with its own database, but they all share one Core: authentication and the user/account model live in Core, each sub-app delegates to Core's verify-user, and an nginx gateway routes each path to its service. One login, one user, many tools — add a service without re-solving auth or tenancy.
GHL data reaches reports through webhooks, not polling: a report service receives GHL webhooks, publishes to Google Pub/Sub and a Redis queue, and RabbitMQ consumers process them into Cube.js models over BigQuery.
The Public Reporting API (/api/ghl/public/v1) authenticates each partner by an X-API-Key mapped to an agency id (sha256, indexed prefix), exposes a static ~20-metric catalog plus report-driven and drilldown endpoints, and reuses the internal metric engine directly instead of a second pipeline.
It's guarded for cost and tenancy: an agency-scoped ~1-hour cache (keyed by metric + date range + agency), a per-agency drilldown rate limit (60 req/60s), CUSTOM date bounds (≤ 400 days), and a uniform 404 for unknown-or-cross-agency resources to block enumeration.
Notifications is the shared delivery layer for every service: any tool posts an event by API key, and Notifications fans it to matching routes, renders a template, and delivers over the right channel — Slack, Discord, email or webhook — asynchronously on a RabbitMQ queue with exponential backoff (30s → 3600s).
A read-only Kinsta + WordPress MCP assistant exposes 50+ tools (over 90 declared, mutations hard-disabled) over SSE via OpenRouter, so an agent can answer 'who did what across the fleet' and pull hosting and error logs — but can never mutate.
04The decision
Options considered
The partner-facing API had to expose report metrics cheaply and tenant-safely over a BigQuery-backed engine that already existed — and already had per-agency quirks.
- ALive-query Cube.js/BigQuery on every request — always current, but slow and it bills real money per call.
- BBuild a separate, duplicated metric pipeline for the public API — a clean contract, but it drifts from the internal engine.
- CA thin authenticated facade that reuses the internal engine, guarded by caching, rate limits and date bounds.
Chose The thin facade over the existing engine.
The trade-off I accepted
A duplicated pipeline would give the public API a clean, stable contract, but it would drift from the internal engine and double the surface to maintain. Reusing the engine behind a thin facade means zero new infra and no drift — at the cost of inheriting the engine's fragility. A thin metric object can return 200-but-empty, and per-agency group-id literals are duplicated inline across several files, so I made the facade fail loud with a 422 rather than serve a misleading zero, and wrapped every read in an agency-scoped cache, a drilldown rate limit and hard date bounds so a partner can't run up a BigQuery bill or enumerate another agency's data.
05The result
- A partner reporting API over 66 Cube.js cubes, served from a ~1-hour agency-scoped cache with a 60 req/60s drilldown limit and ≤ 400-day bounds, so BigQuery cost stays bounded.
- A read-only fleet assistant exposing 50+ MCP tools with every mutation blocked at runtime.
06Stack
- NestJS
- Express
- RabbitMQ
- Cube.js
- BigQuery
- PostgreSQL
- Prisma
- Redis
- MCP
- OpenRouter
- nginx
- React