Technical reference for the server-v2 codebase.
| Concern | Choice |
|---|---|
| Runtime | Node.js 18+ with TypeScript |
| Framework | Express 5 |
| ORM | Prisma 7 (PostgreSQL) |
| Auth | JWT (bcrypt passwords, VerificationToken for OTP) |
| Nodemailer + Resend | |
| Gupshup API | |
| Payments | Paystack |
| File storage | AWS S3 (presigned upload URLs) |
| Real-time | Socket.io v4 |
| Scheduling | node-cron v4 |
| Docs | Swagger UI via swagger-jsdoc |
server-v2/
prisma/
schema.prisma Database schema
prisma.config.ts Prisma 7 config (datasource url lives here, not in schema)
seed.ts Seed script
src/
app.ts Express app + http.Server creation
server.ts Middleware wiring, socket init, cron init
index.ts server.listen
constants/
app.ts API_OBJECTS, APP_NAME, DEFAULT_PAGE_SIZE, token names
countries.ts Static ISO country list (not in DB)
messages.ts All error and success strings
pricing.ts DEFAULT_CREDIT_RATES fallback constant
controllers/ One class per domain, static RequestHandler methods
cronJob/
campaigns.ts Polls every minute, dispatches scheduled campaigns
middlewares/
auth.middleware.ts authenticate — verifies JWT, loads req.user
roles.middleware.ts requireRole, requireVerified, requireAdmin, requireSuperAdmin
routes/ One file per domain, JSDoc Swagger comments on every route
services/ Business logic, one class per domain
socket/
index.ts setupSocket, emitToRoom, getIO
types/ express.d.ts augments Request with req.user
utilities/
common.ts constructResponse, paginateItems, generateRandomString, etc.
encrypt.ts AES-256-GCM encrypt/decrypt (for Gupshup passwords at rest)
file-parser.ts parseCSV, parseXLSX, buildCSV, buildXLSX (exceljs)
markdown.ts renderMarkdownFile — renders .md files as styled HTML
swagger.ts swagger-jsdoc spec config
| Model | Purpose |
|---|---|
Account |
All users. Role: superadmin, admin, business, user |
VerificationToken |
OTP for email verification and password reset. One per email at a time. |
AccountDevice |
Push notification tokens (Expo) |
Business |
1-to-1 with Account. Holds company profile, Gupshup credentials, wallet, campaigns |
Wallet |
1-to-1 with Business. Stored running balance, updated atomically with Transactions |
Transaction |
Audit log for all credit purchases, usage, plan purchases, and withdrawals |
Campaign |
Belongs to Business. scheduledAt (UTC) drives the cron job |
Contact |
Belongs to Business. @@unique([phoneNumber, creatorId]) for dedup on import |
Plan |
Admin-created subscription tiers with per-message-type rates |
UserPlan |
Active plan for a Business. Tracks freeCredit, balance, expiredDate |
AppSetting |
Singleton. Global credit rates, low-credit threshold, affiliate %. Falls back to DEFAULT_CREDIT_RATES constant if empty |
Setting |
Key/value notification preferences per Business or Account |
Notification |
Persisted notifications. receiverId + receiverType identify the recipient room |
ExchangeRate |
Per-country rate. Country list is a static TS constant, not a DB table |
There is no separate Affiliate model. A business becomes an affiliate by setting type = "affiliate". Referred businesses store referredBy (FK to an affiliate Business). Commission is credited to the affiliate's wallet at campaign publish time.
gupshupHSMPassword, gupshupDashboardPassword, and gupshupTwowayPassword are encrypted at rest with AES-256-GCM before writing to the database. The key comes from the ENCRYPTION_KEY environment variable (64-char hex string). Credentials are decrypted only in BusinessService.getDecryptedCredentials(), which is called exclusively from the campaign dispatcher and the Gupshup webhook handler. All HTTP responses mask these fields to "true" or "false".
POST /auth/sign-up
→ Account created (role=business, verified=false)
→ Business + Wallet + default Settings created
→ VerificationToken created
→ OTP email sent
→ JWT returned (limited access until verified)
POST /auth/verify-email (requires JWT)
→ VerificationToken consumed
→ Account.verified = true
→ Admin notified via Notification + Socket.io
POST /auth/sign-in
→ Returns accessToken (14d) + refreshToken (21d)
POST /auth/refresh-token
→ Returns new accessToken
Campaigns store scheduledAt: DateTime? in UTC. null means send immediately on publish.
The cron job in src/cronJob/campaigns.ts runs every minute:
WHERE isScheduled = true
AND isPublished = true
AND isSentOut = false
AND scheduledAt <= now()
All matching campaigns are dispatched via CampaignService.dispatchMessages. There are no nested cron jobs or in-memory maps — a server restart safely picks up any missed campaigns on the next tick.
The client is responsible for converting local time to UTC before sending scheduledAt.
The server never receives file bytes directly. The flow is:
POST /upload/presigned-url → returns { uploadUrl, fileUrl, key }
client uploads to uploadUrl (PUT to S3)
POST /upload/confirm → client sends { url, fileName, size, type }
creates UploadFile record in DB
For contact import from a file, the client can either:
POST /contacts/importfileUrl to POST /contacts/import-file — server fetches and parsesPOST /payments/credit-link → Paystack checkout URL + pending Transaction
user pays on Paystack
POST /webhooks/paystack → charge.success → wallet credited
POST /payments/verify → manual fallback verification after redirect
Plan purchase follows the same pattern using POST /payments/plan-link.
Affiliate withdrawals go through Paystack transfers. The wallet is debited immediately and the Transaction stays pending until transfer.success arrives on the webhook.
Clients join rooms on connect:
| Event | Room key |
|---|---|
joinBusinessRoom(businessId) |
businessId |
joinAdminRoom(userId) |
accountId |
rejoinRooms({ businessId?, userId? }) |
both |
The server emits notification to the appropriate room whenever a Notification is created via NotificationService.createAndEmit. Clients listen on the notification event.
Triggered by: campaign completion, opt-in/out, payment success, business verification, new signup.
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
ACCESS_TOKEN_SECRET |
JWT access token signing key |
REFRESH_TOKEN_SECRET |
JWT refresh token signing key |
ENCRYPTION_KEY |
64-char hex key for AES-256-GCM credential encryption |
PAYSTACK_SECRET_KEY |
Paystack secret key (payments + webhooks) |
AWS_ACCESS_KEY_ID |
S3 uploads |
AWS_SECRET_ACCESS_KEY |
S3 uploads |
AWS_REGION |
S3 region |
AWS_BUCKET_NAME |
S3 bucket |
ALLOWED_ORIGINS |
Comma-separated CORS origins |
BASE_API_ENDPOINT |
Public base URL (used in docs and emails) |
PORT |
Server port (default 4000) |