Saltar al contenido
HA_

Este artículo aún no está disponible en su idioma, por lo que está leyendo el original en inglés.

Context

Advertisers lose budget to bots, VPN, proxy and data-center traffic. A report that arrives weeks later only documents the loss. ClickFortify scores every paid click on pages that carry its script, and acts on the bad ones while the campaign is still spending: flagged IPs are excluded in Google Ads, and suspicious conversions are flagged or filtered before they reach Meta.

I am the lead architect.

Constraints

  • Every advertiser is a workspace. We call them spaces. Accounts, rules and data are scoped per space and must never leak into each other.
  • Detection runs on live traffic, continuously. Collection has to stay fast even when something downstream is slow.
  • The action happens in someone else's system. An exclusion only counts once it exists in the advertiser's Google Ads or Meta account, and those APIs have quotas, limits and their own failure modes.

Architecture

The backend is a set of NestJS services: a tracker that collects clicks, a worker that scores them and syncs exclusions, a webhook service for ad-platform and billing events, and the API behind the Next.js dashboard. They hand work to each other through Redis-backed Bull and BullMQ queues, so each one scales on its own and a slow ad-platform API turns into a backlog instead of an outage.

The worker is split into processors by job: ad clicks, behaviour, device and threat signals, bot tells, Google Ads sync, Meta Ads, reconciliation, billing and email. Scoring combines device, network and behaviour signals, with MaxMind data for IP intelligence. Everything lands in PostgreSQL through Prisma. Spaces share one database and are separated by scope, not by separate databases.

Ad clickTrackerQueuesWorkersAd platformsPostgreSQLAPI
FIG. 01 / A click is collected, queued, scored by workers and synced back to the ad platforms.

The illustration below is a simplified, interactive walk through one click, from arrival to exclusion. The scoring in it is illustrative, not the production model.

Anatomía de un clic bloqueado

Modelo simplificado con fines ilustrativos. No son las reglas ni los pesos de producción.

Activa o desactiva las señales de este clic

Puntuación de riesgo

0 · Permitido

Qué le ocurre a este clic

  1. El Tracker registra el clic y responde de inmediato.
  2. El clic entra en una cola, así que un pico de tráfico nunca ralentiza la recogida.
  3. Un worker lo puntúa a partir de las señales anteriores.
  4. La decisión se guarda junto con los motivos que la justifican.
  5. Ninguna acción. El clic cuenta como tráfico real.

It runs on Kubernetes on GCP and deploys through ArgoCD. Prisma migrations run as an ArgoCD PreSync hook, so the schema is migrated before any new pod starts.

Decisions and alternatives I rejected

No silent changes in a customer's ad account

During the Meta connect work, the suggestion was to create a Meta Pixel automatically when the customer didn't have one. I rejected it. ClickFortify never creates anything in a customer's external account in the background. Any flow that would provision something shows a review step first: an editable name, the account or business it will be created under, what we will do with it, and a Create button the customer presses. Defaults are fine. Silent execution is not: it takes away the customer's say over naming and placement.

Three repositories for GitOps, not one

The original plan kept the Kubernetes manifests inside the application monorepo. I split it into three repositories instead: the app, the manifests ArgoCD deploys, and the platform infrastructure. ArgoCD re-clones the repository on every refresh, and the monorepo is large. The platform also has its own lifecycle and reviewers, and the app's CI has no access to the platform repository at all.

Migrations that fail closed

The migration job runs the image being deployed and is not retried. The alternative, letting Kubernetes retry a failed migration, risks running it again over a half-applied schema. With the PreSync hook, a failed migration aborts the whole sync and keeps its logs, and the pods never run against a schema they don't expect.

What went wrong

The tracker saw the wrong client IP

The tracker sits behind a CDN and a reverse proxy, and sometimes behind Google's own relays as well. We worked out the visitor's IP by counting proxy hops. When the hop count was too low, clicks were recorded with the CDN's edge address and scored as bad. When we raised it, Google's relay became the "visitor", the bot filter treated it as a known bot, and the click was dropped with only a debug log. Hop counting breaks every time the topology changes, so we stopped depending on it and verified the fix on live traffic, with visitor IPs back in local ISP ranges.

A related lesson: a dropped click needs a record. Clicks for an inactive space used to vanish with no error. They are now stored as ignored, with the reason.

Outcome

ClickFortify syncs IP exclusions back to advertisers' ad accounts automatically. Releases go out through GitOps on Kubernetes, and a failed migration stops a release before it reaches the pods.

The move from a single VM to the cluster was a staged cutover. The new stack ran behind a parallel set of stage hostnames, was verified there, and then the production hostnames were repointed in one flip. No data moved, because both sides used the same databases. That also meant the worker stayed at zero replicas on the staging side, since its scheduled jobs would otherwise have run twice against production data. After the flip, live tracker traffic showed real visitor IPs.

The lesson I keep coming back to: detection is the easy half. The hard half is making sure the exclusion actually exists in someone else's system, and noticing quickly when it doesn't.