Skip to content
Asikur Rahman
← All work

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.

GHL WEBHOOKPUB/SUB + REDISRABBITMQCONSUMERSCUBE.JSOVER BIGQUERYPARTNERX-API-KEYPUBLIC APITHIN FACADECACHE · LIMITWEBHOOK INGESTIONREUSES ENGINE · CACHED · RATE-LIMITED
Architecture — Agency Framework

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