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.
- 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
- 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.
- Node.js 24+
- npm 11+
- Docker Desktop (recommended), or locally available PostgreSQL and Redis
npm install
copy .env.example .env
docker compose up -d
npm run prisma:deploy
npm run prisma:seed
npm run start:devOn 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.localeventee@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.
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 stringJWT_SECRET: bearer-token signing secret, at least 32 charactersQR_SIGNING_SECRET: separate ticket-credential HMAC secret, at least 32 charactersREDIS_URL: Redis/BullMQ connection stringFRONTEND_BASE_URL: used in event share and QR check-in URLsCORS_ORIGIN: one or more comma-separated allowed browser originsPAYSTACK_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.
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.
- Add PostgreSQL and Redis services in the same Railway project.
- In the API service's Variables settings, reference the PostgreSQL service's
DATABASE_URLand Redis service'sREDIS_URLusing their private connections. - Set
NODE_ENV=production,TRUST_PROXY=true, and separate randomly generatedJWT_SECRETandQR_SIGNING_SECRETvalues of at least 32 characters each. - Set
PAYSTACK_SECRET_KEY,SMTP_HOST,SMTP_PORT,SMTP_SECURE,SMTP_USER,SMTP_PASS, andSMTP_FROMdirectly in Railway Variables. Never upload.env. - Generate a public Railway domain. Set
FRONTEND_BASE_URLandCORS_ORIGINto the intended frontend origin; for API-only Swagger review, use the API origin. - Deploy, then run
npm run check:connectionsinside the deployed container using Railway SSH. All four services must reportOK. - Open
/api/v1,/docs, and/docs/openapi.jsonon the public HTTPS domain. Configure Paystack's webhook ashttps://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.
npm run start:dev
npm run build
npm run start:prodWith 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.
POST /api/v1/auth/register,POST /api/v1/auth/login,GET /api/v1/auth/meGET /api/v1/events,GET /api/v1/events/:slugPOST /api/v1/events,PATCH /api/v1/events/:idPOST /api/v1/events/:id/publish,DELETE /api/v1/events/:idGET /api/v1/me/events,GET /api/v1/events/:id/attendeesGET|POST /api/v1/events/:eventId/ticket-typesPATCH|DELETE /api/v1/ticket-types/:id
POST /api/v1/orders,GET /api/v1/orders/mine,DELETE /api/v1/orders/:idPOST /api/v1/payments/orders/:orderId/initializePOST /api/v1/payments/:reference/verifyPOST /api/v1/payments/paystack/webhookGET /api/v1/payments/creator/history?eventId=...GET /api/v1/me/tickets,GET /api/v1/tickets/:idPOST /api/v1/check-ins
GET|POST /api/v1/events/:eventId/reminders/defaultsPOST /api/v1/events/:eventId/reminders/personalGET /api/v1/me/reminders,DELETE /api/v1/reminders/:idGET /api/v1/notifications,PATCH /api/v1/notifications/:id/readGET /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 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.
- In the Paystack dashboard, switch to Test Mode and copy the test secret key.
- Set
PAYSTACK_SECRET_KEY=sk_test_...in the local.env. Keep Paystack’s default API URL. - Expose the local server with an HTTPS tunnel for webhook testing.
- In Paystack’s test webhook settings, set the URL to
https://YOUR-TUNNEL/api/v1/payments/paystack/webhook. - Complete checkout using Paystack test data. Eventful validates
x-paystack-signature, then independently calls Paystack’s transaction verification endpoint before confirming an order. - The client may call
/payments/:reference/verifyafter 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.
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.
npm test -- --runInBand
npm run test:e2e
npm run lint
npm run build
npm run prisma:validateUnit 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:e2eOn PowerShell use $env:TEST_DATABASE_URL='...' and $env:DATABASE_URL=$env:TEST_DATABASE_URL.
- “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.