A production-style event ingestion and processing pipeline. Events come in through an authenticated REST API, get queued in Redis via BullMQ, are processed by workers with retry/backoff logic, and results are persisted to PostgreSQL. A React dashboard shows live event status.
POST /events → Redis Queue → Worker → PostgreSQL
↓
Dashboard polls /events
eventflow/
├── api/ Express API — ingestion, auth
│ └── src/
│ ├── config/ DB + Redis + Queue clients
│ ├── controllers/ Request handlers
│ ├── middleware/ Auth, error handling
│ ├── routes/ Route definitions
│ └── services/ Business logic (EventService)
├── worker/ BullMQ worker — processes queued jobs
│ └── src/
│ ├── config/ Redis + DB config
│ └── processors/ Event processor registry + handlers
├── dashboard/ Vite + React — event viewer
│ └── src/
│ ├── components/ UI components
│ ├── hooks/ Data fetching hooks with polling
│ └── lib/ API client
├── shared/ Shared TypeScript types + logger
├── prisma/ Database schema + migrations + seed
└── docker-compose.yml
| Service | Port | Description |
|---|---|---|
| API | 3000 | Event ingestion + REST API |
| Dashboard | 5173 | React UI (served via nginx in prod) |
| PostgreSQL | 5432 | Event storage |
| Redis | 6379 | Job queue (BullMQ) |
| Worker | — | Background job processor |
- Docker + Docker Compose
make(optional but convenient)curlor any HTTP client for testing
git clone <repo>
cd eventflow
cp .env.example .env
# Edit .env if needed (defaults work for local dev)# With Make:
make up
# Without Make:
docker compose up --build -dThis starts: PostgreSQL, Redis, API, Worker, and Dashboard.
Wait ~15 seconds for PostgreSQL to initialize and Prisma migrations to run.
curl http://localhost:3000/health
# {"status":"ok","checks":{"database":"ok","redis":"ok"}}curl -X POST http://localhost:3000/events \
-H "Authorization: Bearer dev_key_123" \
-H "Content-Type: application/json" \
-d '{"type":"user.signup","payload":{"email":"test@example.com","userId":"usr_123"}}'Response:
{
"id": "uuid-here",
"jobId": "uuid-here",
"status": "QUEUED",
"message": "Event accepted and queued for processing."
}Events appear immediately and auto-refresh every 5 seconds.
make seed
# Or: docker compose exec api npx tsx ../prisma/seed.tsmake test-eventsmake metrics
# Or:
curl http://localhost:3000/events/metrics \
-H "Authorization: Bearer dev_key_123"make logs # All service logs
make logs-api # API logs only
make logs-worker # Worker logs only
make list-events # Show latest 5 events
make down # Stop services
make down-v # Stop services + delete volumesAll routes require: Authorization: Bearer <api-key>
Ingest a new event.
curl -X POST http://localhost:3000/events \
-H "Authorization: Bearer dev_key_123" \
-H "Content-Type: application/json" \
-d '{
"type": "order.created",
"payload": {
"orderId": "ord_abc",
"amount": 99.99,
"currency": "USD"
}
}'Validation rules:
type: required, alphanumeric +._:-, max 128 charspayload: JSON object, max 512KBsource: optional string, max 64 chars
Response 202:
{ "id": "...", "jobId": "...", "status": "QUEUED" }List events with filtering and pagination.
GET /events?page=1&pageSize=20&status=FAILED&type=user.signup
Get a single event by ID.
Manually retry a FAILED or DEAD_LETTERED event.
Get aggregated counts by status + success rate.
Health check for DB and Redis.
| Type | Behaviour |
|---|---|
user.signup |
Validates email, simulates welcome email |
order.created |
Simulates inventory update + fulfillment trigger |
payment.processed |
Simulates billing record update (5% failure rate) |
* (any other) |
Generic processor — logs + transforms payload |
Edit worker/src/processors/registry.ts:
const myProcessor: ProcessorFn = async (job) => {
const start = Date.now();
// your logic here
return {
success: true,
processedAt: new Date().toISOString(),
durationMs: Date.now() - start,
output: { handled: true },
};
};
const PROCESSORS: Record<string, ProcessorFn> = {
"my.event.type": myProcessor,
// ...
};Configured in api/src/config/index.ts:
attempts: 3
backoff: exponential, starting at 1s
→ Attempt 1: immediate
→ Attempt 2: 1s delay
→ Attempt 3: 2s delay
→ After 3 failures: status = DEAD_LETTERED
Dead-lettered events can be manually retried from the dashboard or via API.