Skip to content

<- all work

Impact Tracker

A webhook service that turns Linear tickets and merged GitHub pull requests into a Postgres record of what a team shipped.

RoleBackend design and build
Year2026
StackTypeScript, Express 5, Drizzle ORM, PostgreSQL, Webhooks
Impact Tracker pipeline Linear sends an event when an issue moves to Done, and GitHub sends one when a pull request is merged. Both arrive at an Express and TypeScript service that keeps the raw request body. The service checks an HMAC-SHA256 signature and answers 401 without storing anything if it is wrong. Only the events that matter are kept, and everything else gets a 200 and is ignored. The kept events are saved to PostgreSQL through Drizzle in the completed_tasks and merged_prs tables, where unique ids block duplicates. /webhook/linear /webhook/github LINEARissue moves to Done GITHUBpull request merged EXPRESS 5 + TYPESCRIPTraw request body kept for signing HMAC-SHA256 CHECKbad signature: 401, nothing stored KEEP ONLY WHAT MATTERSeverything else: 200 and ignore POSTGRESQL VIA DRIZZLEcompleted_tasks / merged_prsunique ids block duplicates
FIG. 01 From webhook to database.

The problem

Teams ship constantly, but “what did we actually deliver?” is scattered across a ticket tracker and a code host. Reports get written by hand from memory.

What I built

A small Express and TypeScript service with two webhook endpoints:

  • /webhook/linear records an issue (with its story points) when it moves to Done or Merged To Main.
  • /webhook/github records a pull request (repository, additions, deletions) when it is closed and merged.

Both write to PostgreSQL through Drizzle ORM, with unique keys on the Linear issue id and the GitHub PR id so a repeated delivery cannot create duplicates.

Impact Tracker data model Two tables. completed_tasks has id as primary key, linear_issue_id which is unique, title, story_points, completed_at and source. merged_prs has id as primary key, github_pr_id which is unique, title, repository, additions, deletions and merged_at. completed_tasks id uuid PK linear_issue_id text UNIQUE title text story_points int completed_at timestamp source text merged_prs id uuid PK github_pr_id int UNIQUE title text repository text additions int deletions int merged_at timestamp
FIG. 02 The data model, drawn from the Drizzle schema. Highlighted rows are primary or unique keys.

Engineering notes

  • Signatures are verified before anything is parsed. Each request is checked with an HMAC-SHA256 of the raw body against the shared secret, so the endpoints only accept genuine Linear and GitHub events. The raw body is kept for this, which is why the JSON parser is replaced with express.raw.
  • Only the events that matter are stored. Everything else gets a quick 200 and is ignored.
  • Typed tables with Drizzle keep the schema and the queries in one place.

Status

An early-stage backend. Next steps: a small dashboard that charts delivered points and merged PRs over time, constant-time signature comparison, and tests.