Webhook Delivery Tutorial · Module 01 of 10

Foundations & Project Setup

By the end of this tutorial you'll have a Git repo, an HTTP server that can receive requests, a working linter, a one-page design doc sketching the three main tables, and the foundation for reliable webhook delivery.

~2–3 hrsBeginnerNo prior code needed
← Back to Module 01 overview
Before You Start

Prerequisites

  • Node.js 20+ — install from nodejs.org or via nvm / fnm / volta.
  • Gitgit --version should print something.
  • A GitHub account (free) and the gh CLI installed and authenticated, or be ready to create a repo via the web UI.
  • An editor — VS Code is recommended.
  • A database — Postgres 14+ or use Docker: docker run -d -e POSTGRES_PASSWORD=password -p 5432:5432 postgres:16
What You'll Have at the End

Definition of Done

  • A GitHub repo named webhook-delivery with main as the default branch.
  • A Node.js/TypeScript HTTP server running locally on http://localhost:3000.
  • npm run lint and npm run dev both work.
  • A one-page docs/design.md documenting the three main tables: webhooks, events, deliveries.
  • Database migrations setup (Postgres connected and ready).
  • Branch protection on main requiring PRs.
The Steps

Build It

STEP 1

Create the Git repo

From an empty parent folder:

mkdir webhook-delivery && cd webhook-delivery
git init -b main
gh repo create webhook-delivery --public --source=. --remote=origin
# Or create via github.com and clone if you prefer
✓ Verify: git status shows "On branch main".
STEP 2

Initialize Node.js and install dependencies

npm init -y
npm install express typescript ts-node dotenv pg
npm install --save-dev @types/node @types/express eslint prettier
✓ Verify: ls node_modules | grep express shows express is installed.
⚠ Gotcha: Don't commit node_modules/. Create a .gitignore first (see Step 3).
STEP 3

Configure .editorconfig, .gitignore, and linting

Create .gitignore:

node_modules/
.env
.env.local
dist/
*.log
.DS_Store

Create .eslintrc.json:

{
  "extends": "eslint:recommended",
  "parser": "@typescript-eslint/parser",
  "parserOptions": { "ecmaVersion": 2020 },
  "env": { "node": true, "es2020": true },
  "rules": { "no-unused-vars": "warn" }
}

Create .editorconfig:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
✓ Verify: Files exist: ls -la | grep git and ls -la | grep eslint.
STEP 4

Create TypeScript config and a simple "hello" server

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Create src/index.ts:

import express from 'express';

const app = express();
app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Webhook Delivery Service is running' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
✓ Verify: File exists at src/index.ts and contains the server code.
STEP 5

Add npm scripts and test the server

Update package.json with these scripts:

"scripts": {
  "dev": "ts-node src/index.ts",
  "build": "tsc",
  "lint": "eslint src --ext .ts",
  "lint:fix": "eslint src --ext .ts --fix"
}

Run the server:

npm run dev

In another terminal, test it:

curl http://localhost:3000
✓ Verify: curl http://localhost:3000 returns {"message":"Webhook Delivery Service is running"}. Linting passes: npm run lint.
⚠ Gotcha: If linting fails, run npm run lint:fix to auto-fix most issues.
STEP 6

Write the one-page design doc

Create docs/design.md:

# Webhook Delivery Service — Design Doc

## Problem
Teams need a reliable way to send events (webhooks) to customer endpoints. If the customer's server is down, events must be retried; if it keeps failing, events go to a dead-letter queue for manual recovery.

## Goals
- Reliably deliver events to customer endpoints
- Support exponential backoff retries (1s, 4s, 16s, 64s, 256s)
- Implement dead-letter queue for permanent failures
- Track delivery status and provide audit logs
- Support at least 1000 events/sec to 100 webhooks

## Non-Goals
- End-to-end encryption (HTTPS covers this)
- Built-in webhook transformations
- Event sampling/filtering (can add later)

## Data Model

### webhooks table
```
id (UUID)
url (string) - customer endpoint
secret (string) - for HMAC signing
event_types (string[]) - which events this webhook receives
created_at (timestamp)
updated_at (timestamp)
```

### events table
```
id (UUID)
type (string) - e.g., "order.created"
payload (JSON) - the event data
timestamp (timestamp)
created_at (timestamp)
```

### deliveries table
```
id (UUID)
webhook_id (UUID) - foreign key
event_id (UUID) - foreign key
status (enum) - pending, processing, success, failed
attempts (int) - how many retries so far
last_error (string)
next_retry_at (timestamp)
created_at (timestamp)
updated_at (timestamp)
```

## Idempotency Strategy
Each delivery has a unique `delivery_id`. Customers should use this to deduplicate if they receive the same event twice. Server guarantees: best-effort delivery (at-least-once, not exactly-once).

## Open Questions
- How long to retain events in the log?
- Should we support batch deliveries?
- Authentication: API key, OAuth, or both?
✓ Verify: File exists at docs/design.md and describes the three tables.
STEP 7

Commit and enable branch protection

git add .
git commit -m "init: scaffold webhook delivery service

- Set up TypeScript, Express, linting
- Basic health-check endpoint
- Design doc with data model"
git push -u origin main

On GitHub, go to Settings → Branches and enable branch protection for main. Require PR reviews and status checks.

✓ Verify: git log --oneline shows your commit. GitHub web shows the repo with branch protection enabled.
Next Steps

Ready for Module 02?

You've laid the foundation. Next, you'll implement the core webhook API: registration endpoints and event emission. Head over to Module 02 when you're ready.