← Back to all posts

ReQL: adding new event sources without redeploying

We have a tiny in-house DSL — ReQL — that lets us add a new webhook source in minutes, hot-reloaded into production, without touching the ingestion server. Here's why.

Paul Mou··7 min read
EngineeringArchitecture

The hard part of an incident-correlation tool is not the correlation. It is the ingestion. Every customer's deploy webhook looks different. Every alerting tool has its own JSON shape. Every feature-flag vendor names their fields differently. And the moment you ship a hard-coded parser per source, you've signed up for an integration backlog that grows linearly with your customer base forever.

We knew we did not want to spend the rest of our lives writing, reviewing, and deploying parser PRs. So we built a tiny in-house DSL — we call it ReQL, the Regle Query Language — that lets us add a new source in minutes and hot-reload it into production without redeploying the ingestion server.

The wrong way we almost shipped first

The obvious first cut: a Go package per source, each one a function that takes an opaque JSON blob and returns our internal Change struct. Tested, type-safe, fine.

The problem shows up the moment you have ten sources and the eleventh customer asks for theirs. Adding a source means: write Go, write tests, open PR, get review, run CI, deploy ingestion. For a two-person team that wants to say yes to integration requests in a Slack thread, the round-trip is wrong by an order of magnitude.

Worse: a bug in one parser brings the whole service down. We did not want one bad customer's webhook to break ingestion for everyone else.

What ReQL looks like

ReQL is a small expression language for transforming JSON. It is spiritually similar to jq, with three things added on purpose: typed locals, an HTTP-RPC step, and crash-safe error semantics. Here's the package that handles a Vercel deploy webhook:

LET DeployId    : String = jsonExtract(EventData, "$.id");
LET ProjectName : String = jsonExtract(EventData, "$.payload.project.name");
LET Url         : String = jsonExtract(EventData, "$.payload.url");
LET State       : String = jsonExtract(EventData, "$.payload.deployment.state");
LET CreatedMs   : Int    = jsonExtractInt(EventData, "$.createdAt");
LET Actor       : String = jsonExtract(EventData, "$.payload.user.username");

LET Change = {
  "id":          DeployId,
  "kind":        "deploy",
  "service":     ProjectName,
  "environment": "vercel/prod",
  "created_at":  CreatedMs / 1000,
  "actor":       Actor,
  "description": concat("Vercel deploy → ", State),
  "url":         Url,
  "raw":         jsonStringify(EventData)
};

httpRpc("twg.change", Change);

That's the whole integration. Twenty lines. The schema for our internal Change object is built from string concatenation and JSON pointers, the HTTP-RPC step posts it to our core ingestion pipeline, and we're done.

The three properties that make this worth the cost

1. Hot reload, no redeploy

ReQL packages live in a separate git repo. The ReQL server watches it via a webhook from GitHub. When a package changes, the server compiles and swaps it in within a couple of seconds. The ingestion pipeline never restarts. Customer-facing iteration loops dropped from "open a PR, wait an hour" to "push a commit, watch it work."

2. Per-package isolation

Every ReQL package runs in its own evaluator instance. A buggy package — null dereference, infinite loop, malformed JSON — fails to that customer's event. It cannot bring down the ingestion server and it cannot affect anyone else's events. This is the property that lets us be aggressive about saying yes to one-off integrations.

3. Crash-safe semantics

We deliberately have no try/catch in ReQL. Any failing expression silently substitutes null for that field. The output is then run through a strict schema validator at the pipeline boundary. Either the change is fully valid and we ingest it, or it's not and we drop it into a dead-letter queue we review daily. This sounds reckless and turned out to be the most reliable property of the whole system. There is no partialingestion to debug.

The implementation, briefly

The ReQL server is a small Rust service. Parser is hand-written because the grammar is tiny enough that a parser generator would be more code than the parser. Evaluator is a tree-walker — no compilation to bytecode, no JIT, no SIMD JSON, no clever tricks. Latency is dominated by the network round-trip to the downstream service, not by interpretation. We measured before deciding not to optimize. Highly recommend that order of operations.

Built-in functions are a small library focused on JSON transformation: jsonExtract, jsonExtractInt, jsonStringify, iso8601Str2UnixSec, concat, joinArray, regexExtract, and one escape hatch: httpRpc to call into Go/TypeScript when something is genuinely beyond ReQL. That escape hatch has been used twice in eighteen months.

What we'd do differently

We over-typed the language. ReQL has explicit String, Int, Bool annotations on every LET. It made parser errors marginally better and made the language meaningfully more annoying to write. Next major version: inferred types, like jq.

We'd also bake in jq compatibility. ReQL's JSON paths are similar but not identical to jq's, and people who already know jq trip on the differences. There is no good reason for the divergence.

Why share this

We are not selling ReQL. It is not an open-source release. The reason this is on our blog is that the "tiny DSL with hot reload and per-package isolation" pattern is generally useful for ingestion-heavy products, and we don't see it written about enough. If you're an early-stage team staring down a growing integration backlog, the answer is not "hire an integrations engineer." The answer is probably "put a domain-specific language between you and the customer's JSON."

Tired of guessing what changed?

Regle is the tool we wished we had at 2 AM. Sign up — 5-minute setup, 14-day free trial, no credit card.