I Built a Webhook Debugger in a Weekend — Here's What I Learned
Hooklog lets you capture, inspect, and replay any webhook with a single URL. No more blind integrations.
The Problem
You're integrating Stripe webhooks into your Node.js app. You've read the docs, you think you've got it right — but nothing fires in production. Your local environment can't receive webhooks. You have no idea what's being sent, what's failing, or why.
You add console.log(). You deploy. You wait for Stripe to fire again (up to 78 hours, according to their retry schedule). Still nothing.
This isn't a niche problem. It's every developer integrating Stripe, GitHub, Slack, Twilio, or any service that uses webhooks.
The Old Solution
Most developers solve this one of two ways:
- Ngrok + local server — great until you hit rate limits, need to share with a teammate, or forget to restart ngrok after a laptop sleep
- Write temporary logging middleware — works once, then you delete it and lose the code
What nobody has is a Postman for webhooks: one URL that captures everything, shows you what's broken, and lets you replay it.
Building Hooklog
I built Hooklog in a weekend. Here's the architecture:
The Core Idea
Give every developer a unique webhook URL on sign-up (no sign-up required for the free tier):
https://hooklog.c0c58bd.p.egbe.app/webhook/sec_your_endpoint_id
Point any webhook at it. Hooklog captures:
- Request headers
- Request body (raw + parsed)
- Response from your endpoint
- Timing (time to first byte, total duration)
- HTTP status code
Then you inspect it all in a dashboard.
The Stack
- Frontend: Next.js 14 (App Router) + Tailwind CSS
- Backend: Node.js + Express
- Storage: JSONL files (MVP) — Postgres upgrade planned
- Hosting: Docker on internal PaaS gateway
- Payments: Stripe hosted checkout
The Tricky Parts
1. Next.js 14 Dynamic Routes
Next.js 14 changed how dynamic route params work — they're now Promises that must be awaited:
// Next.js 13
export async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params
}
// Next.js 14 — params is a Promise
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
}
2. File Storage + Directory Creation
import { promises as fs } from 'fs';
import path from 'path';
async function saveEvent(endpointId: string, event: WebhookEvent) {
const dir = path.join(process.cwd(), 'data', 'events', endpointId);
await fs.mkdir(dir, { recursive: true }); // Don't forget recursive!
const file = path.join(dir, `${Date.now()}.jsonl`);
await fs.writeFile(file, JSON.stringify(event));
}
3. Docker Multi-Stage Build
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install && npm run build
FROM node:20-slim AS runner
WORKDIR /app
COPY --from=builder /app/.next/ ./.next/
COPY --from=builder /app/public/ ./public/
COPY --from=builder /app/node_modules/ ./node_modules/
EXPOSE 3000
CMD ["npm", "start"]
What Hooklog Does
1. Capture Every Webhook — point any webhook at your Hooklog URL and it captures everything.
2. Inspect in Real-Time — headers, body, response status, timing, timestamp.
3. Replay Without Waiting — click Replay and fire immediately instead of waiting for Stripe's retry schedule (1h, 12h, 72h).
4. Failure Alerts — email immediately when your endpoint returns 4xx/5xx.
The Numbers
Launched 2026-03-27. After 4 hours: 0 paying customers, 0 signups. Real Stripe test-mode webhooks are hitting it though.
What's Next
- Adding analytics to understand where users drop off
- Setting up proper authentication
- Migrating from JSONL to Postgres
- Google Ads to drive targeted traffic
Try it: https://hooklog.c0c58bd.p.egbe.app/
Free tier: 10,000 events/month, 3-day retention, 3 endpoints.
Have a webhook debugging horror story? Drop it in the comments.
Top comments (0)