Skip to content

Repository files navigation

Eventful API

Eventful is a production-minded NestJS backend for publishing events, selling or issuing tickets, securely admitting attendees, sending reminders, and reporting creator analytics. A user is not permanently assigned a creator/eventee role: any authenticated account may create its own events and attend other events. Private creator operations are protected by event ownership checks.

Implemented capabilities

  • JWT registration, login, and current-user endpoints with normalized email and bcrypt password hashing
  • Draft/publish/cancel event lifecycle, custom free or paid ticket types, public discovery, stable slugs, and social share links
  • PostgreSQL-authoritative inventory with serializable transactions, overall capacity, per-ticket allocation, per-user limits, sales windows, and expiring paid reservations
  • Server-calculated Decimal(12,2) prices and immutable order-item price snapshots
  • Free-order confirmation and Paystack initialization, verification, signed webhooks, multiple attempts, exact kobo conversion, and idempotent ticket fulfilment
  • One ticket row per purchased quantity; signed QR credentials are reproducible for their owner without storing usable plaintext secrets
  • Creator-only, event-bound, single-use check-in enforced both in application logic and by a unique database constraint
  • Persistent creator-default and ticket-holder personal reminders using Redis/BullMQ; in-app notifications remain available when SMTP is not configured
  • Creator-overall and per-event analytics with precise ticket, unique-eventee, check-in, ticket-type, and successful-revenue metrics
  • Redis caching for published discovery/detail and analytics, with mutation-driven invalidation and TTL fallback
  • Helmet, configured CORS, strict DTO validation, bearer auth, webhook raw-body verification, and global/route-specific rate limits
  • Swagger UI plus OpenAPI JSON

Stack and architecture

  • Node.js 24, TypeScript 6 strict mode, NestJS 12
  • PostgreSQL 17 and Prisma 7 with the PostgreSQL driver adapter
  • Redis 7 and BullMQ 6
  • Jest 30/ts-jest, Swagger/OpenAPI, Paystack, Nodemailer, qrcode

Controllers handle HTTP concerns and delegate to domain services. Prisma models transactional state; no duplicate attendee or analytics tables are used. Redis is an optimization and job transport, never the source of truth for inventory. Paid reservations and fulfilment run at PostgreSQL SERIALIZABLE isolation with write-conflict retry. A late successful payment is fulfilled only if capacity still exists; otherwise its payment is recorded and the order is cancelled for explicit refund handling instead of overselling.

Prerequisites

  • Node.js 24+
  • npm 11+
  • Docker Desktop (recommended), or locally available PostgreSQL and Redis

Local setup

npm install
copy .env.example .env
docker compose up -d
npm run prisma:deploy
npm run prisma:seed
npm run start:dev

On macOS/Linux, replace copy with cp. Change JWT_SECRET and QR_SIGNING_SECRET in .env; they must each be at least 32 characters in production and must be different. The development defaults exist only to make local tooling approachable.

The seed creates these local accounts, using SEED_PASSWORD (default Password123!):

  • creator@eventful.local
  • eventee@eventful.local

Use npm run prisma:migrate while authoring a new development migration. Use npm run prisma:deploy to apply committed migrations in CI/production.

Environment

Configured Windows workspace

This workspace's ignored .env uses local PostgreSQL on 127.0.0.1:55432 and Redis on 127.0.0.1:56379. The application database is eventful and the integration database is eventful_test; Redis databases 0 and 1 separate their caches and queues. All four migrations are applied to both databases, and the application database contains the demo seed accounts below. These loopback services are for local development.

After a reboot, run npm run services:local, then npm run check:connections and npm run start:prod (run npm run build after code changes). Preserve .tmp/verification-postgres and .tmp/redis-verification, which contain the local service data and binaries. The Docker setup above remains the portable alternative and uses ports 5432/6379; adjust .env when switching.

npm run check:connections checks PostgreSQL, Redis, Paystack authentication, and SMTP connectivity. It prints no credentials and exits nonzero if a service is missing or fails. The Paystack probe uses the read-only balance endpoint. SMTP uses Nodemailer's connection verification; it does not send email or prove that a sender/recipient will accept delivery.

To enable actual payments and email, set PAYSTACK_SECRET_KEY to your Paystack test secret and supply SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, and an approved SMTP_FROM in .env. Port 465 normally uses SMTP_SECURE=true; port 587 uses false with STARTTLS. Restart the API after changing settings. Never commit .env. The integration suite mocks Paystack and disables SMTP, so its success does not verify external payments or email delivery.

For Gmail, enable two-step verification, create a 16-character Google app password, and use the Gmail account as SMTP_USER and SMTP_FROM. Spaces copied into a Gmail app password are removed automatically; ordinary Gmail account passwords are rejected by Google SMTP.

See .env.example. Required production values are:

  • DATABASE_URL: PostgreSQL connection string
  • JWT_SECRET: bearer-token signing secret, at least 32 characters
  • QR_SIGNING_SECRET: separate ticket-credential HMAC secret, at least 32 characters
  • REDIS_URL: Redis/BullMQ connection string
  • FRONTEND_BASE_URL: used in event share and QR check-in URLs
  • CORS_ORIGIN: one or more comma-separated allowed browser origins
  • PAYSTACK_SECRET_KEY: Paystack test or live secret; never expose it to the frontend

Optional settings include PORT, JWT_EXPIRES_IN, cache/reservation durations, proxy trust, Paystack base URL, SMTP settings, and test service URLs. Without SMTP, email attempts are skipped but persistent in-app notifications are still created. SMS/PUSH enum values reserve a compatible future schema; V1 accepts IN_APP and EMAIL reminder requests.

Railway deployment

Deploy this repository as a Railway service using the committed Dockerfile and railway.json. The container startup script applies Prisma migrations before starting the API, including on hosts that do not apply the pre-deploy setting. The Railway configuration also specifies /api/v1 as its health check.

  1. Add PostgreSQL and Redis services in the same Railway project.
  2. In the API service's Variables settings, reference the PostgreSQL service's DATABASE_URL and Redis service's REDIS_URL using their private connections.
  3. Set NODE_ENV=production, TRUST_PROXY=true, and separate randomly generated JWT_SECRET and QR_SIGNING_SECRET values of at least 32 characters each.
  4. Set PAYSTACK_SECRET_KEY, SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS, and SMTP_FROM directly in Railway Variables. Never upload .env.
  5. Generate a public Railway domain. Set FRONTEND_BASE_URL and CORS_ORIGIN to the intended frontend origin; for API-only Swagger review, use the API origin.
  6. Deploy, then run npm run check:connections inside the deployed container using Railway SSH. All four services must report OK.
  7. Open /api/v1, /docs, and /docs/openapi.json on the public HTTPS domain. Configure Paystack's webhook as https://YOUR_DOMAIN/api/v1/payments/paystack/webhook.

Connection checks verify Paystack authentication and SMTP connectivity/authentication; they do not create a charge or send email. Use Paystack test checkout and a separate email delivery test when verifying those complete flows.

See Railway configuration reference.

Running locally

npm run start:dev
npm run build
npm run start:prod

With the default port:

  • API base: http://localhost:3000/api/v1
  • Swagger UI: http://localhost:3000/docs
  • OpenAPI JSON: http://localhost:3000/docs/openapi.json
  • Health probe: GET http://localhost:3000/api/v1

Use Authorize in Swagger with the JWT returned by register/login.

API overview

Authentication and events

  • POST /api/v1/auth/register, POST /api/v1/auth/login, GET /api/v1/auth/me
  • GET /api/v1/events, GET /api/v1/events/:slug
  • POST /api/v1/events, PATCH /api/v1/events/:id
  • POST /api/v1/events/:id/publish, DELETE /api/v1/events/:id
  • GET /api/v1/me/events, GET /api/v1/events/:id/attendees
  • GET|POST /api/v1/events/:eventId/ticket-types
  • PATCH|DELETE /api/v1/ticket-types/:id

Orders, payments, tickets, and admission

  • POST /api/v1/orders, GET /api/v1/orders/mine, DELETE /api/v1/orders/:id
  • POST /api/v1/payments/orders/:orderId/initialize
  • POST /api/v1/payments/:reference/verify
  • POST /api/v1/payments/paystack/webhook
  • GET /api/v1/payments/creator/history?eventId=...
  • GET /api/v1/me/tickets, GET /api/v1/tickets/:id
  • POST /api/v1/check-ins

Reminders, notifications, and analytics

  • GET|POST /api/v1/events/:eventId/reminders/defaults
  • POST /api/v1/events/:eventId/reminders/personal
  • GET /api/v1/me/reminders, DELETE /api/v1/reminders/:id
  • GET /api/v1/notifications, PATCH /api/v1/notifications/:id/read
  • GET /api/v1/analytics/creator, GET /api/v1/analytics/events/:eventId

Swagger contains DTO fields, enum values, authentication requirements, route summaries, query parameters, and validation constraints.

Ticket, QR, and check-in design

Ticket numbers are 128-bit random identifiers. The QR credential is an HMAC-signed versioned payload containing that random number, and only its SHA-256 hash is stored. An owner can retrieve the ticket later because the server deterministically recreates the credential with QR_SIGNING_SECRET; a database leak alone cannot forge it. Rotating that secret invalidates existing QR credentials, so production rotation needs an intentional migration/versioning plan.

The backend serves a public event page at /events/:slug and a creator check-in page at /check-in. Set FRONTEND_BASE_URL to the backend's public origin to use these built-in pages.

QR images point to {FRONTEND_BASE_URL}/check-in#credential=...; the fragment keeps the credential out of HTTP request URLs. The page also accepts older query-string QR links. Creators sign in, select their event, and explicitly confirm admission. Credentials and login tokens are not saved in browser storage. The existing check-in API verifies ticket validity, event ownership, and previous admission; a second scan returns HTTP 409.

Paystack test-mode setup

  1. In the Paystack dashboard, switch to Test Mode and copy the test secret key.
  2. Set PAYSTACK_SECRET_KEY=sk_test_... in the local .env. Keep Paystack’s default API URL.
  3. Expose the local server with an HTTPS tunnel for webhook testing.
  4. In Paystack’s test webhook settings, set the URL to https://YOUR-TUNNEL/api/v1/payments/paystack/webhook.
  5. Complete checkout using Paystack test data. Eventful validates x-paystack-signature, then independently calls Paystack’s transaction verification endpoint before confirming an order.
  6. The client may call /payments/:reference/verify after redirect for faster UX. It uses the same server verification/idempotent fulfilment path; client claims are never trusted.

For live mode, use a live secret only in the deployment secret store, change the webhook to the production HTTPS URL, and test refund/operational alerts for the documented late-payment case.

Reminders and Redis

Every reminder is stored in PostgreSQL before its BullMQ job is added. Jobs have stable IDs, retries, exponential backoff, and persistent Redis state. On application bootstrap, scheduled database reminders are reconciled back into the queue, covering an application restart or temporary queue outage. Creator defaults resolve current ticket holders at delivery time, so attendees who register after reminder creation are included. Notification creation is unique per reminder/user to make retries safe.

Tests and quality checks

npm test -- --runInBand
npm run test:e2e
npm run lint
npm run build
npm run prisma:validate

Unit tests cover normalized secure auth behavior, server-side order pricing, capacity rejection, immediate free fulfilment, one-ticket-per-quantity issuance, idempotent issuance, QR forgery rejection, signed-webhook rejection, and duplicate check-in.

The e2e suite loads .env and runs when TEST_DATABASE_URL is supplied. It uses unique accounts per run and never clears existing tables. It covers free/paid journeys, concurrent inventory reservations, ownership restrictions, cached event edits, refund-required payment records, reminders, analytics, Swagger, and rate limits. Tests have a 30-second timeout for multi-request journeys against real PostgreSQL and Redis. Prepare an isolated migrated database, Redis, and run:

set TEST_DATABASE_URL=postgresql://eventful:eventful@localhost:5432/eventful_test?schema=public
set DATABASE_URL=%TEST_DATABASE_URL%
npm run prisma:deploy
npm run test:e2e

On PowerShell use $env:TEST_DATABASE_URL='...' and $env:DATABASE_URL=$env:TEST_DATABASE_URL.

Product assumptions and current limits

  • “Applying” means obtaining a ticket; V1 has no manual approval workflow.
  • A paid event may offer zero-cost complimentary ticket types, which are confirmed like free tickets.
  • Cancelling preserves audit/payment history and invalidates active tickets instead of deleting the event.
  • Only the event creator can scan in V1. Delegated scanner accounts are a natural later extension.
  • Refund initiation and reconciliation are not automated. A verified payment arriving after its reservation and after capacity has been reused is recorded, not fulfilled, and requires creator/operator refund action.
  • Email is optional and intentionally cannot block ticketing. SMS and push transports are schema-compatible but not implemented.
  • The committed e2e journeys require external PostgreSQL and Redis; unit tests require neither and never contact Paystack.

About

NestJS event management and ticketing backend

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages