Skip to content
FactorQX
intermediateNode.jsExpressRedisDocker

Build a TradingView-to-Broker Webhook Bridge

Architect, build, deploy, and monitor a small service that receives TradingView alerts and forwards validated messages to a broker integration layer.

3 min read

This guide walks the full lifecycle of a small but realistic automation service: a bridge that receives TradingView alert webhooks, validates them, and hands them to a broker-integration layer. We cover architecture, code, deployment, and monitoring.

This is an engineering guide. The bridge moves and validates messages; it does not decide what to trade and is not advice. Connecting to live execution is your responsibility and carries real financial risk.

Architecture

The design separates three concerns so each can fail independently:

  1. Ingress — a tiny HTTP endpoint that authenticates and validates alerts.
  2. Queue — a Redis list/stream that decouples ingress from processing.
  3. Worker — a process that pops messages and calls the broker layer.

Decoupling matters: TradingView retries on non-200 responses, so the ingress must respond fast and never block on slow downstream work.

The ingress endpoint

The endpoint authenticates with a shared secret, validates the payload, enforces idempotency, and enqueues — then returns immediately.

ingress.js
import express from "express";
import { createClient } from "redis";
 
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
 
const app = express();
app.use(express.json({ limit: "16kb" }));
 
app.post("/webhook", async (req, res) => {
  if (req.body?.secret !== process.env.WEBHOOK_SECRET) {
    return res.status(401).json({ error: "unauthorized" });
  }
  const { id, symbol, action } = req.body;
  if (!id || !symbol || !["buy", "sell"].includes(action)) {
    return res.status(422).json({ error: "invalid payload" });
  }
  // Idempotency: SET NX returns null if the key already existed.
  const fresh = await redis.set(`seen:${id}`, "1", { NX: true, EX: 86400 });
  if (!fresh) return res.json({ ok: true, duplicate: true });
 
  await redis.lPush("alerts", JSON.stringify({ id, symbol, action }));
  return res.json({ ok: true });
});
 
app.listen(8080);

The worker

The worker blocks on the queue and processes one message at a time, with retries handled explicitly rather than by re-queuing blindly.

worker.js
import { createClient } from "redis";
 
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
 
while (true) {
  const popped = await redis.blPop("alerts", 0); // block until a message
  const msg = JSON.parse(popped.element);
  try {
    await handle(msg); // your broker-integration call goes here
  } catch (err) {
    console.error("processing failed", msg.id, err);
    await redis.lPush("alerts:dead", JSON.stringify({ msg, error: String(err) }));
  }
}
 
async function handle(msg) {
  // Validate again, check risk limits, then call the broker layer.
  console.log("handling", msg.symbol, msg.action);
}

Deployment

Package both processes with Docker and run them with the queue:

docker-compose.yml
services:
  ingress:
    build: { context: ., dockerfile: Dockerfile.ingress }
    environment: [REDIS_URL=redis://redis:6379, WEBHOOK_SECRET]
    ports: ["8080:8080"]
    depends_on: [redis]
  worker:
    build: { context: ., dockerfile: Dockerfile.worker }
    environment: [REDIS_URL=redis://redis:6379]
    depends_on: [redis]
  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]

Put the ingress behind HTTPS (a reverse proxy such as Caddy or Nginx). Never expose the webhook secret in client code.

Monitoring

At minimum, track:

  • Queue depth (LLEN alerts) — a rising backlog means the worker can't keep up.
  • Dead-letter count (LLEN alerts:dead) — any growth needs investigation.
  • Ingress latency and 4xx/5xx rates — TradingView retries amplify errors.

Export these as metrics (e.g. to Prometheus) and alert on backlog and dead-letter growth.

Scaling

For higher throughput, switch the Redis list to a Redis Stream with consumer groups so multiple workers share the load with acknowledgements and replay. Add per-symbol rate limits and a circuit breaker around the broker call so a single failing dependency can't cascade.

You now have a decoupled, observable bridge — a solid foundation you can extend with risk checks and a real execution layer at your own discretion and risk.

Educational engineering guide. This walkthrough teaches how to build software. It is not investment advice, a trading signal, or a guarantee of results. See our disclaimer.