EventForge is a webhook ingestion + delivery app built with Next.js (App Router), PostgreSQL + Prisma, and Upstash QStash for reliable background delivery and replays.
It lets you:
- Define webhook endpoints (a target URL + metadata)
- Ingest events (store payload + headers)
- Deliver events to the destination URL
- Track delivery status (PENDING / DELIVERED / FAILED) and delivery attempts
- Replay a webhook event (re-queue delivery via QStash)
- Framework: Next.js (App Router)
- Language: TypeScript / React
- Database: PostgreSQL
- ORM: Prisma (with
@prisma/adapter-pg) - Queue / Delivery: Upstash QStash
- UI: Tailwind CSS + Radix UI (shadcn-style components)
- Next.js App (UI + API routes)
- Dashboard pages under
app/dashboard/* - Server actions in
app/actions.ts - API routes under
app/api/*
- Dashboard pages under
- PostgreSQL
- Stores endpoints, webhook events, and delivery attempts
- QStash
- Handles asynchronous delivery and retries
- Event replays are re-queued into QStash
- Webhook received (ingestion endpoint)
- App persists event data in Postgres via Prisma
- App queues a delivery job to QStash
- QStash calls
POST /api/worker - Worker fetches the stored event + endpoint and POSTs to the target URL
- Worker stores a DeliveryAttempt and updates event status to DELIVERED or FAILED
- If failed, worker returns 500 so QStash can retry (based on QStash settings)
- User clicks replay in UI
- Server action
replayWebhook(eventId)updates status back to PENDING - Server action publishes a QStash job to call
/api/worker - Worker re-delivers and logs another attempt
The database schema lives in prisma/schema.prisma and includes:
WebhookEndpointtargetUrl,secret,provider,description, timestamps
WebhookEvent- links to an endpoint
- stores
payloadandheadersas JSON status:PENDING | DELIVERED | FAILED
DeliveryAttempt- linked to a
WebhookEvent - stores
responseStatusand truncatedresponseBody
- linked to a
.
├── app/
│ ├── actions.ts # server actions (ex: replay)
│ ├── api/
│ │ ├── worker/route.ts # delivers events to target URL
│ │ └── ingest/ # ingestion route folder exists (implementation may vary)
│ ├── components/
│ │ ├── AutoRefresh.tsx
│ │ └── ReplayButton.tsx
│ ├── dashboard/
│ │ ├── page.tsx # lists endpoints
│ │ ├── [endpointId]/ # exists in repo (dynamic route)
│ │ └── event/[eventId]/ # exists in repo (dynamic route)
│ ├── globals.css
│ └── layout.tsx
├── components/
│ └── ui/ # shared UI components (Radix/shadcn style)
├── lib/
│ ├── prisma.ts # Prisma client + pg adapter
│ └── utils.ts
├── prisma/
│ ├── schema.prisma
│ ├── seed.ts
│ └── migrations/
├── public/
├── package.json
└── next.config.ts- Node.js (recommended: latest LTS)
- PostgreSQL database
- An Upstash QStash account + token
npm installCreate a .env file in the project root:
# PostgreSQL
DATABASE_URL="postgresql://USER:PASSWORD@HOST:PORT/DB?schema=public"
# Upstash QStash
QSTASH_TOKEN="YOUR_QSTASH_TOKEN"Notes:
DATABASE_URLis required by Prisma and the pg adapter (lib/prisma.ts).QSTASH_TOKENis required for replay + queue publishing (app/actions.ts).
Generate Prisma client:
npx prisma generateRun migrations (choose the command that matches your workflow):
npx prisma migrate dev(Optional) Seed sample data:
npx prisma db seedThe UI dashboard mentions seeding if no endpoints exist.
npm run devOpen:
http://localhost:3000
This route is intended to be called by QStash (and can be used locally for testing).
Request body:
{ "eventId": "..." }Behavior:
- Loads the webhook event + endpoint from DB
- Sends
POSTto the endpoint’stargetUrl - Stores a
DeliveryAttempt - Updates status:
DELIVEREDif 2xx elseFAILED - Returns
500on failed delivery to encourage QStash retry
Shows a list of webhook endpoints and total event counts per endpoint.
Other dynamic routes exist in the repo:
app/dashboard/[endpointId]/...app/dashboard/event/[eventId]/...
(These typically show per-endpoint events and per-event details / attempts.)
- Prisma client is created in
lib/prisma.tsand cached globally in non-production to avoid hot-reload connection issues. - Replay uses a server action (
app/actions.ts) and publishes a QStash job targeting/api/worker. - Tailwind CSS is configured via
tailwind.config.js.
From package.json:
npm run dev # start dev server
npm run build # build production bundle
npm run start # start production server
npm run lint # run eslint