ServicePro FSMS is a production-ready, secure and scalable backend for dispatch-managed field service work. Customers submit service requests, an admin reviews them and assigns a verified technician with conflict-free scheduling, the technician works the job through a tracked status lifecycle, and the customer pays online (Stripe or bKash) and leaves feedback.
| Item | Value |
|---|---|
| Backend Repo | servicepro-fsms |
| Live API | https://servicepro-fsms-1.onrender.com |
| Health Check | https://servicepro-fsms-1.onrender.com/health |
| Postman Collection | postman/ServicePro-FSMS.postman_collection.json |
| Frontend Repo | servicepro-frontend |
| Live Frontend | https://.vercel.app |
| Admin Email | [email protected] |
| Admin Password | Admin123! |
| Demo Video | Watch Demo Video |
β οΈ These credentials are for testing and demonstration only.
| Role | Password | |
|---|---|---|
| Admin | [email protected] |
Admin123! |
| Customer | [email protected] |
Customer123! |
| Technician | [email protected] |
Technician123! |
| Technology | Purpose |
|---|---|
| Runtime | Node.js 20+ (ESM) |
| Framework | Express 5 with TypeScript |
| Database | PostgreSQL |
| ORM | Prisma 7 (driver-adapter pattern via @prisma/adapter-pg) |
| Auth | JWT access + rotating refresh tokens, Google OAuth, bcrypt |
| Security | Helmet, CORS, express-rate-limit, role-based access control |
| Validation | Zod |
| Payments | Stripe (cards) & bKash (sandbox Tokenized Checkout) |
| File Uploads | Multer (memory) β Cloudinary |
| Cache | Redis (optional: caching + invalidation) |
| Real-time | WebSocket (ws) live notifications |
| Logging | Pino + Morgan |
| Deployment | Render (REST + WebSocket) / Vercel (REST only) |
Customer ββΊ Service Request (PENDING) ββΊ Admin review ββΊ Technician assigned + visit scheduled (ASSIGNED)
ββΊ Technician accepts (ACCEPTED) ββΊ Work started (IN_PROGRESS) ββΊ Work completed (COMPLETED)
ββΊ Service report + invoice amount ββΊ Payment (Stripe / bKash) ββΊ Customer feedback
Every transition is checked against a fixed state map and the role allowed to trigger it, and each one writes a JobLog and an AuditLog row in the same database transaction.
| From | Allowed next states | Who can trigger |
|---|---|---|
PENDING |
ASSIGNED, CANCELLED |
Admin (assign / reject), Customer (cancel) |
ASSIGNED |
ACCEPTED, CANCELLED |
Assigned technician, Admin, Customer |
ACCEPTED |
IN_PROGRESS, CANCELLED |
Assigned technician, Admin, Customer |
IN_PROGRESS |
COMPLETED, CANCELLED |
Assigned technician, Admin (cancel also open to the customer) |
COMPLETED |
- (terminal; payment follows) | - |
CANCELLED |
- (terminal) | - |
Layered architecture (routes β controllers β services β Prisma) for a clean separation of concerns:
servicepro-fsms/
βββ src/
β βββ config/
β β βββ env.ts # typed environment config
β β βββ prisma.ts # Prisma 7 client + pg driver adapter
β β βββ redis.ts # optional Redis client
β β βββ cloudinary.ts
β β βββ logger.ts # pino
β βββ controllers/ # thin HTTP layer
β β βββ admin.controller.ts
β β βββ auth.controller.ts
β β βββ job.controller.ts
β β βββ notification.controller.ts
β β βββ payment.controller.ts
β β βββ review.controller.ts
β β βββ technician.controller.ts
β β βββ user.controller.ts
β βββ services/ # business logic + transactions
β β βββ admin.service.ts
β β βββ auth.service.ts
β β βββ job.service.ts # state machine, conflict-safe assignment
β β βββ notification.service.ts
β β βββ payment.service.ts # Stripe + bKash, idempotent finalisation
β β βββ review.service.ts
β β βββ technician.service.ts # suggestion scoring
β β βββ upload.service.ts
β β βββ user.service.ts
β βββ routes/ # one router per resource + index.ts
β βββ validators/ # Zod schemas per resource
β βββ middleware/
β β βββ auth.ts # JWT authentication
β β βββ role.ts # requireRole(...)
β β βββ validate.ts # Zod validation
β β βββ rateLimiter.ts
β β βββ upload.ts # Multer
β β βββ errorHandler.ts # 404 + structured errors
β βββ errors/AppError.ts
β βββ utils/ # apiResponse, asyncHandler, jwt, auditLog, cache, stateMachine
β βββ ws/notifier.ts # WebSocket connection registry + broadcast
β βββ types/express.d.ts
β βββ constants.ts # transition map, pagination defaults
β βββ app.ts # Express app (middleware + routes)
β βββ server.ts # REST only (local dev)
β βββ ws-server.ts # REST + WebSocket (Render)
β βββ server.vercel.ts # serverless entrypoint (Vercel)
βββ prisma/
β βββ schema.prisma
β βββ seed.ts # admin + demo customer + demo technician
βββ postman/ServicePro-FSMS.postman_collection.json
βββ prisma.config.ts
βββ render.yaml
βββ vercel.json
βββ package.json
PostgreSQL with Prisma 7. Main models: User, RefreshToken, TechnicianProfile, Availability, Job, JobLog, Attachment, ServiceReport, Payment, TechnicianReview, Notification, AuditLog, SystemSetting.
- Relationships & constraints: one-to-one
JobβPayment/ServiceReport/TechnicianReviewenforced with@unique; unique gateway identifiers onPayment(stripePaymentId,bkashPaymentId,bkashTrxId,transactionId) back webhook idempotency. - Indexes target real query patterns:
Job(status, scheduledAt),Job(technicianId),Job(customerId),AuditLog(entity, entityId),Notification(userId, isRead),RefreshToken(userId, revokedAt), and more. - Soft delete: users are deactivated with
deletedAt+isActiveinstead of being removed. - Payment methods:
STRIPEandBKASHonly - there is deliberately no cash or pay-later option.
- Helmet: secure HTTP headers
- CORS: restricted to the configured
FRONTEND_URL(credentials supported) - Rate limiting: global limiter plus a stricter limiter on login/register
- Input validation: every mutating endpoint validated with Zod, returning field-level errors
- Password hashing: bcrypt
- JWT authentication: short-lived access token + rotating refresh tokens (stored hashed; each refresh revokes the previous token)
- Google OAuth: ID token verified server-side
- Role-based access control:
CUSTOMER,TECHNICIAN,ADMINenforced per route and re-checked on resource ownership in the service layer - Webhook security: Stripe webhook signature verification on the raw request body
- Audit trail: critical actions recorded with actor, action, entity and details
- Secrets: environment variables only, never committed
Base URL: https://servicepro-fsms-1.onrender.com/api/v1
All responses use one structure:
{ "success": true, "message": "Operation successful", "data": {} }
{ "success": false, "message": "Validation failed", "errors": [{ "field": "email", "message": "Invalid email" }] }Protected routes use Authorization: Bearer <accessToken>.
| Method | Endpoint | Description | Access |
|---|---|---|---|
POST |
/auth/register |
Register as customer or technician | Public |
POST |
/auth/login |
Email/password login | Public |
POST |
/auth/google |
Login / sign up with a Google ID token | Public |
POST |
/auth/refresh-token |
Rotate refresh token, get a new access token | Public |
POST |
/auth/logout |
Revoke the refresh token | Public |
| Method | Endpoint | Description | Access |
|---|---|---|---|
GET |
/users/me |
Current user profile | Private |
PATCH |
/users/me |
Update profile | Private |
POST |
/users/me/avatar |
Upload avatar (Cloudinary) | Private |
| Method | Endpoint | Description | Access |
|---|---|---|---|
GET |
/technicians |
List technicians (?skill&available&verified&page&limit) |
Public |
GET |
/technicians/suggest?jobId= |
Ranked technician suggestions for a job | Admin |
GET |
/technicians/:id |
Technician profile | Public |
GET |
/technicians/:id/reviews |
Technician reviews | Public |
PATCH |
/technicians/me/availability |
Toggle accepting jobs | Technician |
PATCH |
/technicians/me/location |
Update current location | Technician |
PATCH |
/technicians/me/skills |
Update skills & service areas | Technician |
PUT |
/technicians/me/weekly-availability |
Replace weekly working hours | Technician |
| Method | Endpoint | Description | Access |
|---|---|---|---|
POST |
/jobs |
Create a service request | Customer |
GET |
/jobs |
List jobs (role-scoped; ?status&category&priority&sortBy&sortOrder&page&limit) |
Private |
GET |
/jobs/search?q= |
Search title / description / address | Admin |
GET |
/jobs/:id |
Job details with timeline, payment, report, attachments | Private |
PATCH |
/jobs/:id/review |
Approve (record review) or reject a pending request | Admin |
POST |
/jobs/:id/assign |
Assign technician + schedule visit (conflict-checked) | Admin |
PATCH |
/jobs/:id/reschedule |
Move the visit (conflict-checked) | Admin |
PATCH |
/jobs/:id/status |
Accept / start / complete / cancel | Technician* |
POST |
/jobs/:id/cancel |
Cancel a job | Owner / Admin |
DELETE |
/jobs/:id |
Soft-delete a job (not allowed while work is active) | Admin |
POST |
/jobs/:id/service-report |
Submit or update the service report | Technician |
POST |
/jobs/:id/attachments |
Upload a photo / document | Private |
POST |
/jobs/:id/review-feedback |
Rate the technician after completion | Customer |
* Accept/start/complete are technician-only; cancellation is allowed for the job's customer, assigned technician or an admin.
| Method | Endpoint | Description | Access |
|---|---|---|---|
GET |
/payments |
Payment history (role-scoped, ?status&page&limit) |
Private |
POST |
/payments/initiate |
Start a Stripe PaymentIntent or bKash payment | Customer |
GET |
/payments/:id |
Payment details / status | Private |
GET |
/payments/bkash/callback |
bKash redirect callback (finalises the payment) | Gateway |
POST |
/payments/webhook |
Stripe webhook (signature verified, idempotent) | Gateway |
| Method | Endpoint | Description | Access |
|---|---|---|---|
GET |
/notifications |
My notifications | Private |
PATCH |
/notifications/:id/read |
Mark as read | Private |
Live push is available over WebSocket at /ws?token=<accessToken> on the Render deployment.
| Method | Endpoint | Description |
|---|---|---|
GET |
/admin/users |
List users (?role&page&limit) |
PATCH |
/admin/users/:id/role |
Change a user's role |
DELETE |
/admin/users/:id |
Deactivate (soft delete) a user |
GET |
/admin/technicians/pending |
Technicians awaiting verification |
PATCH |
/admin/technicians/:id/verify |
Approve or reject a technician |
GET |
/admin/dashboard-stats |
Platform statistics (Redis-cached) |
GET |
/admin/audit-logs |
Audit trail (?entity&page&limit) |
PUT |
/admin/settings |
Create / update a system setting |
Plus GET /health for uptime checks. 44 documented endpoints in total.
Node.js 20+, PostgreSQL (e.g. Neon), and optionally Redis.
git clone https://github.com/Sumayea104/servicepro-fsms.git
cd servicepro-fsms
npm install # runs `prisma generate` automatically
cp .env.example .env # then fill in the values below
npx prisma migrate dev --name init
npx prisma db seed # admin + demo customer + demo technician
npm run dev # REST API on http://localhost:5000
# npm run dev:ws # REST API + WebSocket| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET, JWT_REFRESH_SECRET |
Token signing secrets |
JWT_ACCESS_EXPIRES, JWT_REFRESH_EXPIRES |
Token lifetimes (default 15m / 7d) |
FRONTEND_URL |
Allowed CORS origin + bKash redirect target |
GOOGLE_CLIENT_ID |
Google sign-in |
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
Stripe payments (test mode) |
BDT_PER_USD |
BDTβUSD rate used to convert the job price for Stripe card payments (default 120); bKash charges BDT as-is |
BKASH_BASE_URL, BKASH_USERNAME, BKASH_PASSWORD, BKASH_APP_KEY, BKASH_APP_SECRET, BKASH_CALLBACK_URL |
bKash sandbox |
CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET |
File uploads |
REDIS_URL |
Optional caching |
ADMIN_EMAIL, ADMIN_PASSWORD |
Seeded admin credentials |
| Script | Description |
|---|---|
npm run dev / dev:ws |
Development (REST / REST + WebSocket) |
npm run build |
Generate the Prisma client and compile TypeScript |
npm start / start:ws |
Run the compiled server |
npx prisma migrate deploy |
Apply migrations in production |
npx prisma db seed |
Seed demo data |
- Render (Primary):
render.yamlrunsprisma generate && prisma migrate deploy && npm run buildand startsnpm run start:ws. This runs both the REST API and WebSocket (ws) live notifications in a single long-lived process. - Vercel (Frontend & Serverless REST): Optimized for client-side frontend hosting. Note that WebSocket connections require the Render primary host for real-time features.
- Webhook Configuration:
- Stripe: Register
https://servicepro-fsms-1.onrender.com/api/v1/payments/webhookin the Stripe Dashboard. - bKash: Set
BKASH_CALLBACK_URLin environment variables tohttps://servicepro-fsms-1.onrender.com/api/v1/payments/bkash/callback.
- Stripe: Register
-
Preventing double-booked technicians:
- Challenge: Two dispatchers assigning the same technician to overlapping slots at the same moment can both pass a simple "is there a conflict?" check.
- Solution: Assignment and rescheduling run inside a transaction that first takes a Postgres advisory lock (
pg_advisory_xact_lock) scoped to the technician, then checks for overlapping active jobs before committing, so concurrent attempts are serialised and the loser gets a clean409 Conflict.
-
Idempotent, signature-verified payments:
- Challenge: Gateways retry webhooks, and Stripe's signature check fails if the body is JSON-parsed first.
- Solution: The webhook route is mounted with
express.raw()before the global JSON parser. Payment finalisation checks the current status and uses unique gateway ids, so a replayed event is a no-op rather than a double credit. Retry attempts are tracked on thePaymentrow.
-
Dual payment gateways (Stripe & bKash):
- Challenge: Card payments (PaymentIntent, webhook-driven) and bKash (redirect + token grant β create β execute) have completely different flows and currencies.
- Solution: One payment service with a shared
finalizePaymenttransaction that updates the payment, writes the audit log and notifies the user, regardless of gateway; the currency is recorded per payment.
-
Role-based security without repeated checks:
- Challenge: Three roles with different views of the same resources (a customer must only see their own jobs, a technician only assigned ones).
- Solution:
authenticate+requireRole(...)middleware guard routes, and the service layer scopes queries and re-checks ownership, so data access is safe even if a route is misconfigured.
-
Prisma 7 driver adapter & ESM:
- Challenge: Prisma 7 moves connection handling to a driver adapter and generates an ESM client into the source tree; NodeNext ESM also requires explicit
.jsimport extensions. - Solution: Configured
@prisma/adapter-pginconfig/prisma.ts, generated the client onpostinstall/build, and added avercel-buildscript so serverless deployments generate it before compiling.
- Challenge: Prisma 7 moves connection handling to a driver adapter and generates an ESM client into the source tree; NodeNext ESM also requires explicit
-
Cross-origin refresh tokens:
- Challenge: An httpOnly refresh cookie on the API domain is not sent from a frontend on another domain.
- Solution: Refresh tokens are also returned in the response body and accepted from the request body; they are stored hashed server-side and rotated on every use.
- Concurrency control in SQL: advisory locks inside transactions are a simple, reliable way to serialise a critical section per resource.
- Payment engineering: treat gateway callbacks as untrusted and repeatable - verify, then apply changes idempotently.
- Designing around a state machine: one transition map plus per-role permissions keeps workflow rules in one place and makes audit logging automatic.
- Deployment trade-offs: persistent connections (WebSocket) need a long-running host, while serverless suits stateless REST.
- Postman Collection:
postman/ServicePro-FSMS.postman_collection.json- Import it into Postman, run Auth β Login (Admin) and Login (Customer) first; access tokens will be saved to collection variables automatically.
# Health check
curl [https://servicepro-fsms-1.onrender.com/health](https://servicepro-fsms-1.onrender.com/health)
# Login as the demo admin
curl -X POST https://servicepro-fsms-1.onrender.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"Admin123!"}'
# Use the returned accessToken
curl https://servicepro-fsms-1.onrender.com/api/v1/admin/dashboard-stats \
-H "Authorization: Bearer <accessToken>"- Log in as Customer β
POST /jobsto create a request. - Log in as Admin β
GET /technicians/suggest?jobId=β¦, thenPOST /jobs/:id/assignwith a time slot (try the same slot twice to see the409). - Log in as Technician β
PATCH /jobs/:id/status(ACCEPTEDβIN_PROGRESSβCOMPLETEDwithfinalCost) andPOST /jobs/:id/service-report. - As Customer β
POST /payments/initiate(Stripe or bKash), thenPOST /jobs/:id/review-feedback. - As Admin β
GET /admin/audit-logsandGET /admin/dashboard-stats.
- Sumayea Rahman
- GitHub: Sumayea104
- Email: [email protected]
For any questions or issues, please open an issue or contact the developer.
If you found this project helpful, please give it a β on GitHub!