EventPulse is a full-stack campus event platform for capacity-aware registrations, waitlists, QR passes, live entry scanning, analytics, and real-time organizer/volunteer dashboards.
frontend/- Next.js App Router app.backend/- Node.js, Express, Prisma, PostgreSQL, Redis, Kafka, Socket.IO API.docker-compose.yml- Local PostgreSQL, Redis, Kafka/Zookeeper, frontend, and backend services.
cd backend
npm install
cp .env.example .env
npm run prisma:generate
npm run prisma:migrate -- --name init_eventpulse_schema
npm run seed
npm run devThe backend runs on http://localhost:4000 by default.
docker compose up -d postgres redis kafka zookeeper
docker compose up frontend backendPostgreSQL is mapped to host port 5433 to avoid common local conflicts. Inside Docker, services use postgres:5432.
cd backend
npm run prisma:generate
npm run prisma:migrate
npm run prisma:studio
npm run seedRun the RBAC/ABAC authorization suite with:
cd backend
npm run test:authzThe suite uses Jest and Supertest against the real Express app, JWT auth middleware, CASL policies, route middleware, and Prisma-backed service ownership checks. It seeds isolated admin, organizer, volunteer, and student users plus organizer-owned events, then verifies student restrictions, organizer ownership boundaries, volunteer scan scope, and admin access. Run it against a migrated test database; the test data uses unique authz-* emails and cleans itself up after completion.
To avoid accidental writes to shared databases, the suite runs only when DATABASE_URL points to localhost, the database name clearly contains eventpulse_test, or AUTHZ_TEST_DATABASE_URL is provided:
AUTHZ_TEST_DATABASE_URL=postgresql://eventpulse:eventpulse@localhost:5433/eventpulse_test npm run test:authzRemote database runs require explicit opt-in with AUTHZ_ALLOW_REMOTE_DATABASE=true.
Auth:
POST /api/auth/registerPOST /api/auth/loginGET /api/auth/me
Venues:
POST /api/venuesGET /api/venuesGET /api/venues/:idPATCH /api/venues/:idDELETE /api/venues/:idGET /api/venues/:id/schedule
Events:
POST /api/eventsGET /api/eventsGET /api/events/:idPATCH /api/events/:idDELETE /api/events/:id
Registrations and waitlist:
POST /api/events/:id/registerPOST /api/events/:id/cancelGET /api/events/:id/registration-statusGET /api/events/:id/waitlistPOST /api/events/:id/promote-next
Passes and check-in:
GET /api/events/:id/passPOST /api/checkin/scanGET /api/events/:id/checkins
Analytics:
GET /api/analytics/events/:idGET /api/analytics/venuesGET /api/analytics/checkins
Notifications:
GET /api/notificationsGET /api/notifications/unread-countPATCH /api/notifications/:id/readPATCH /api/notifications/read-all
Health:
GET /health
Users register or log in with email/password. Passwords are hashed with bcryptjs. Login returns a JWT with:
{
"userId": "...",
"role": "STUDENT"
}Protected routes use Authorization: Bearer <token>. Public registration can create STUDENT, ORGANIZER, and VOLUNTEER; ADMIN cannot be created through public registration.
Backend route inputs are validated at the API boundary with Zod. Shared schemas live in backend/src/validation/requestSchemas.js and are applied through backend/src/middleware/validateRequest.middleware.js for request bodies, route params, and query strings. Invalid requests return 400 with structured field-level details before reaching service logic.
The backend uses Helmet to apply security response headers before CORS and API routes. The policy includes a conservative Content Security Policy, X-Frame-Options/frame-ancestors deny framing, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and cross-origin isolation/resource policies suitable for an API backend.
Strict-Transport-Security is enabled only when NODE_ENV=production so local HTTP development keeps working. FRONTEND_URL is used in the CSP connect-src allowlist alongside the backend origin.
EventPulse uses Redis-backed fixed-window rate limits for sensitive routes. Login and registration are limited by IP and email to slow brute-force attempts. Authenticated limits protect QR scans, special entry, pass generation, notification mutations, and admin/organizer write routes. Responses include RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers.
Default limits can be tuned with environment variables:
RATE_LIMIT_AUTH_LOGIN_IP_LIMIT=20
RATE_LIMIT_AUTH_LOGIN_IP_WINDOW_SECONDS=900
RATE_LIMIT_AUTH_LOGIN_EMAIL_LIMIT=8
RATE_LIMIT_AUTH_LOGIN_EMAIL_IP_LIMIT=5
RATE_LIMIT_AUTH_REGISTER_IP_LIMIT=10
RATE_LIMIT_AUTH_REGISTER_IP_WINDOW_SECONDS=3600
RATE_LIMIT_QR_SCAN_USER_LIMIT=60
RATE_LIMIT_QR_SCAN_USER_WINDOW_SECONDS=60
RATE_LIMIT_PASS_GENERATION_USER_LIMIT=30
RATE_LIMIT_NOTIFICATION_MUTATION_USER_LIMIT=60
RATE_LIMIT_ADMIN_WRITE_USER_LIMIT=60Backend authorization is centralized with CASL in backend/src/authorization/ability.js. Route middleware uses coarse abilities such as create Event, read GateFlow, and manage Venue, while service methods use object-aware policies for ownership-sensitive records such as events, analytics, waitlists, crew access, check-ins, passes, registrations, and notifications.
Admins can manage all resources. Organizers can create events and manage owned event resources. Volunteers can scan and read operational entry data. Students can register, cancel, read their own passes, and read their own notifications. PostgreSQL remains the source of truth for ownership checks before object-level CASL authorization is evaluated.
High-risk write endpoints require an Idempotency-Key header so client retries do not repeat side effects. EventPulse stores the key, authenticated user, route, method, request fingerprint, response status/body, and expiry in PostgreSQL. Matching retries replay the original successful response with Idempotency-Replayed: true; reusing the same key with a different request returns 409.
Protected endpoints using idempotency:
POST /api/events/:id/registerPOST /api/events/:id/cancelPOST /api/events/:id/promote-nextPOST /api/events/:id/checkins/scanPOST /api/events/:id/checkins/special-entryPOST /api/events/:id/crewPATCH /api/events/:id/crew/:crewAccessIdDELETE /api/events/:id/crew/:crewAccessIdPATCH /api/notifications/:id/readPATCH /api/notifications/read-all
Example:
curl -X POST http://localhost:4000/api/events/event-id/register \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: register-event-id-user-id-1"Keys expire after IDEMPOTENCY_KEY_TTL_SECONDS seconds, defaulting to 24 hours. Clean expired rows with:
cd backend
npm run cleanup:idempotencyStudents register for OPEN or LIVE events before registrationDeadline. Registration uses Redis lock lock:event:{eventId}:registration and a Prisma transaction to prevent overbooking. If capacity is available, the system allocates the first available seat and creates a CONFIRMED registration.
If an event is full and waitlist capacity remains, the student receives a WAITLISTED registration and a WaitlistEntry with the next queue position. Cancelling a confirmed registration releases the seat and promotes the first WAITING waitlist entry by position ASC.
Confirmed students fetch GET /api/events/:id/pass. The backend signs a QR payload using HMAC with JWT_SECRET, stores only the token hash, and returns a QR image data URL. Volunteers, organizers, and admins scan via POST /api/checkin/scan. The scan path uses rate limits, QR token verification, Redis scan locks, and the unique CheckIn.registrationId constraint to reject duplicate entry.
Clients join event rooms with:
join-event-roomleave-event-room
Room format:
event:{eventId}
Server emitted events:
capacity-updatedregistration-updatedwaitlist-updatedcheckin-updatedentry-rate-updatedno-show-releasednotification-creatednotification-read
Redis is used for:
- registration locks
- QR scan locks
- fixed-window rate limits
- fast live counters:
event:{eventId}:registeredCountevent:{eventId}:checkedInCountevent:{eventId}:waitlistCount
PostgreSQL remains the source of truth. Redis counters can be rebuilt with database sync helpers.
EventPulse stores per-user notifications for registration confirmations, waitlist joins and promotions, cancellations, check-ins, and crew access changes. Authenticated clients can fetch /api/notifications, read the navbar badge count through /api/notifications/unread-count, and mark one or all notifications as read. Socket.IO pushes notification-created and notification-read events to each authenticated user room so the badge and /notifications center update live.
Kafka is used for async streams, not source-of-truth storage. Publish failures are logged and do not crash API requests.
Domain services write Kafka messages through a transactional outbox table before Kafka publish. State changes such as registrations, cancellations, waitlist promotions, check-ins, no-show releases, and crew access updates create OutboxEvent rows in the same Prisma transaction as the PostgreSQL mutation. A separate publisher worker reads pending rows, publishes them to Kafka, and marks them as published only after Kafka acknowledges the send.
Run the outbox publisher locally with:
cd backend
npm run publisher:outboxOutbox retries use exponential backoff. Configure them with:
OUTBOX_MAX_ATTEMPTS=10
OUTBOX_PUBLISH_BATCH_SIZE=25
OUTBOX_PUBLISH_POLL_INTERVAL_MS=2500
OUTBOX_PUBLISH_BASE_BACKOFF_MS=1000
OUTBOX_PUBLISH_MAX_BACKOFF_MS=60000
OUTBOX_PROCESSING_TIMEOUT_MS=300000Published Kafka messages preserve the existing topic names and JSON payload shape. The publisher adds Kafka headers eventpulse-outbox-id and eventpulse-outbox-attempt so consumers can de-duplicate if needed. Delivery is at least once: if the process crashes after Kafka accepts a message but before PostgreSQL marks the row as published, the publisher may retry that same outbox row.
Topics:
eventpulse.registration.createdeventpulse.waitlist.joinedeventpulse.waitlist.promotedeventpulse.registration.cancelledeventpulse.checkin.completedeventpulse.security.scan_failedeventpulse.no_show.releasedeventpulse.crew.access_grantedeventpulse.crew.access_updatedeventpulse.crew.access_revokedeventpulse.crew.special_entry_used
Kafka consumers run in dedicated consumer groups:
eventpulse-registration-consumereventpulse-checkin-consumereventpulse-crew-consumer
Run them locally with:
cd backend
npm run consumer:kafkaConsumer processing keeps PostgreSQL as the source of truth. Handlers verify referenced records, resync Redis counters for capacity-affecting topics, and write event-scoped audit logs.
Retry topics:
eventpulse.retry.registrationeventpulse.retry.checkineventpulse.retry.crew
Dead-letter topics:
eventpulse.dlq.registrationeventpulse.dlq.checkineventpulse.dlq.crew
Failed messages are wrapped with the original topic, original payload, original timestamp, attempt count, retry timestamp, consumer group, and serialized error. After KAFKA_CONSUMER_MAX_ATTEMPTS, the message is moved to the matching DLQ topic.
Kafka message contracts are centralized in backend/src/utils/kafkaSchemas.js and enforced with AJV before publishing and before consumer handlers run. Domain messages keep the existing JSON shape:
{
"eventId": "event-id",
"userId": "user-id-or-null",
"registrationId": "registration-id-or-null",
"timestamp": "2026-06-22T12:00:00.000Z",
"metadata": {}
}eventpulse.security.scan_failed allows eventId to be omitted because rejected scans may contain unusable or missing event data. Other domain topics require eventId, timestamp, and metadata. Validation failures are not published on the producer path; consumer-side validation failures are routed through the retry/DLQ flow with structured error details.
Registration, cancellation, waitlist promotion, no-show release, and QR scans are protected with Redis locks and Prisma transactions. Database unique constraints provide a final safety net for one registration per user/event, one waitlist entry per user/event, and one check-in per registration.
Fenwick Tree is used for efficient time-range check-in analytics over bucketed event entry data. The optimized time-range path powers GET /api/analytics/events/:id/time-range without replacing PostgreSQL as the source of truth.
Trie is used for fast autocomplete across events, venues, zones, and categories.
Graph utilities are used to model venue zones and gate loads, enabling least-crowded gate recommendations and simple crowd-flow routing.
Organizers can assign selected students as event-specific organizers, crew members, performers, speakers, volunteer helpers, or VIP entries. Access is scoped to a single event and does not change the student’s global STUDENT role. Each crew access record can specify a special gate and an optional note that volunteers can see during scanning. Crew access updates emit live Socket.IO events to the event room and publish best-effort Kafka stream events.
Run manual workers or queue-backed workers:
cd backend
npm run worker:noshow
npm run worker:analytics
npm run consumer:kafka
npm run scheduler:bullmq
npm run worker:bullmqworker:noshow marks unscanned confirmed registrations as NO_SHOW after a grace period, releases seats, promotes waitlisted users, updates counters, and publishes Kafka events.
worker:analytics computes aggregate event, venue, and check-in metrics and logs them.
BullMQ is used for scheduled and retryable background jobs on top of the existing Redis service. Kafka remains the async domain-event stream; BullMQ handles jobs that need retries, delays, or repeat schedules.
Queues:
eventpulse-event-lifecycleeventpulse-notificationseventpulse-analytics
Job types:
event-no-show-releaseruns after event start plusNO_SHOW_GRACE_MINUTES.process-no-showsrepeats everyNO_SHOW_REPEAT_MSas a safety sweep.registration-deadline-remindercreates organizer in-app reminder jobs before registration closes.event-start-remindercreates attendee in-app reminder jobs before event start.create-notificationwrites in-app notifications through the notification service.analytics-aggregaterepeats everyANALYTICS_REPEAT_MS.
Run npm run scheduler:bullmq once during deployment or local boot to register repeatable jobs and delayed jobs for upcoming events. Run npm run worker:bullmq as a long-running worker process. Jobs use exponential backoff, bounded completed/failed retention, and stable job IDs for event reminders so event updates replace pending schedules.
The backend uses Pino for structured JSON logs and OpenTelemetry for traces. Local development works without an OpenTelemetry collector because tracing is disabled by default.
Environment variables:
LOG_LEVEL=info
OTEL_ENABLED=false
OTEL_SERVICE_NAME=eventpulse-backend
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces
OTEL_DIAG_LOGGING=falseWhen OTEL_ENABLED=true, EventPulse auto-instruments Node HTTP/Express, PostgreSQL, Redis/ioredis, KafkaJS, Socket.IO, and Pino where supported by the OpenTelemetry auto-instrumentation package. The backend also creates manual spans for Kafka publish/consume operations, BullMQ job processing, and Socket.IO emits. Logs include request IDs and active trace/span IDs when a trace context exists.
For local traces, start an OTLP-compatible collector and set OTEL_ENABLED=true plus OTEL_EXPORTER_OTLP_ENDPOINT. Without an endpoint, tracing can still be enabled for local context propagation, but spans are not exported.
The backend exposes Prometheus metrics at GET /metrics using prom-client.
Metrics include default Node.js process metrics plus:
- HTTP request count and duration by method, normalized route pattern, and status.
- Domain counters for registrations, waitlist joins/promotions, cancellations, check-ins, scan failures, crew access changes, notifications, and no-show releases.
- Kafka publish success/failure counters by topic.
- Outbox enqueue/publish/retry/failure counters, current outbox status gauges, and oldest pending outbox age.
- BullMQ job completed/failed counters by queue and job name.
- Optional BullMQ queue state gauges when
PROMETHEUS_BULLMQ_QUEUE_GAUGES=true. - Idempotency started/completed/replayed/conflict/in-flight/failure counters.
Metric labels are intentionally low-cardinality and do not include user IDs, event IDs, request IDs, or raw request paths.
Local Prometheus scrape example:
scrape_configs:
- job_name: eventpulse-backend
metrics_path: /metrics
static_configs:
- targets: ["localhost:4000"]All seeded users use password password123.
admin@iiita.ac.in-ADMINorganizer@iiita.ac.in-ORGANIZERvolunteer@iiita.ac.in-VOLUNTEERstudent1@iiita.ac.in-STUDENTstudent2@iiita.ac.in-STUDENTstudent3@iiita.ac.in-STUDENTstudent4@iiita.ac.in-STUDENTstudent5@iiita.ac.in-STUDENT
cd frontend
npm install
cp .env.example .env.local
npm run devFrontend environment variables:
NEXT_PUBLIC_API_URL=http://localhost:5000
NEXT_PUBLIC_SOCKET_URL=http://localhost:5000The frontend keeps the futuristic EventPulse spatial operations UI while integrating with the backend APIs. Login and registration store the JWT and user profile in local storage, attach Authorization: Bearer <token> to API requests, and update navigation by role.
Integrated pages:
/login/register/events/events/[id]/notifications/pass/[eventId]/organizer/dashboard/organizer/events/new/organizer/events/[id]/volunteer/scan/admin/venues/admin/analytics
Role routing:
STUDENT->/eventsORGANIZER->/organizer/dashboardVOLUNTEER->/volunteer/scanADMIN->/admin/venues
Unauthorized users are sent to /login. Authenticated users with the wrong role see an access-denied operations panel.
- Login as
organizer@iiita.ac.in. - Create an event from
/organizer/events/new. - Login as
student1@iiita.ac.in. - Register from
/events/[id]. - Open
/pass/[eventId]and copy the demo QR token. - Login as
volunteer@iiita.ac.in. - Scan the copied token from
/volunteer/scan. - Scan it again to verify duplicate-entry rejection.
- Login as
admin@iiita.ac.in. - View
/admin/venuesand/admin/analytics. - Open
/organizer/events/[id]to watch live capacity and check-in updates.
WebSocket behavior:
- Organizer event control rooms and the volunteer scanner use
socket.io-client. - Clients join rooms with
join-event-roomand leave withleave-event-room. - Live events update capacity, registration, waitlist, check-in, entry-rate, and no-show signals.
Common frontend fixes:
- If API calls fail, confirm
NEXT_PUBLIC_API_URLpoints to the backend. - If sockets stay offline, confirm
NEXT_PUBLIC_SOCKET_URLpoints to the backend Socket.IO server. - If protected pages redirect, login again and confirm the demo user has the expected role.
- If QR scan fails, generate a fresh pass because QR tokens are signed and expire at event end time.
- Register/login.
- Create IIITA venue.
- Create IIITA event.
- Detect venue conflict.
- Register student.
- Fill event capacity.
- Move users to waitlist.
- Cancel confirmed registration.
- Auto-promote waitlisted user.
- Generate QR pass.
- Scan QR.
- Reject duplicate QR scan.
- Check live counters.
- Verify Kafka logs.
- Run no-show worker.