Team Performance Rewards Platform
Overview
A multi-client gamification platform enabling organizations to structure teams hierarchically, incentivise members through a points-based coaching and reinforcement economy, and manage subscriptions — delivered as three interfaces sharing a single API: a React Native mobile app, a React admin dashboard, and a Node.js REST API.
| Domain | B2B Team Performance & Gamification |
|---|---|
| Role | Lead Engineer & System Architect |
| Team | Small cross-functional team (2–3 engineers) |
| Stack | Node.js, Express, MongoDB, Redis, Socket.io, React, React Native, Stripe |
| Scale | 13 feature modules, 50+ API endpoints, 15+ collections, 4 external integrations |
What I Built
A multi-client platform with three interfaces sharing one API:
| Client | Technology | Audience | Purpose |
|---|---|---|---|
| Mobile App | React Native (iOS + Android) | Team members, coaches, owners | Day-to-day operations — actions, prizes, timeline |
| Admin Dashboard | React SPA | Platform administrators | Client management, subscriptions, billing |
| REST API | Node.js / Express / MongoDB | Both clients | Centralised business logic, auth, real-time |
By the numbers:
- 13 feature modules in the API
- 50+ API endpoints with fine-grained permissions
- 15+ database collections
- 4 external service integrations (Stripe, Twilio, SendGrid, OneSignal)
- Real-time layer via Socket.io
- Full CI/CD with automated staging deploys
3. System Architecture
3.1 Production Architecture (As Built)
3.2 Scalable Target Architecture (Evolution Path)
While the production system ran as a monolith, the modular design was intentional — each module is a bounded context ready to extract into a service. Here’s the architecture I would evolve towards at scale:
Key scalability decisions in the evolution:
| Concern | Current | Scaled Version | Why |
|---|---|---|---|
| Compute | Single PM2 process | Service-per-module behind load balancer | Independent scaling per traffic pattern |
| Auth | Server-side sessions in MongoDB | JWT + refresh tokens with Redis session store | Stateless services, horizontal scaling |
| Points ledger | Direct writes to Balance collection | Event-sourced transactions via message queue | Audit trail, eventual consistency, replay |
| Notifications | Synchronous in request path | Async via message queue (fan-out) | Don’t block user requests on email/SMS/push |
| Files | GridFS in MongoDB | S3 with presigned URLs + CDN | Offload bandwidth, reduce API load |
| Search | MongoDB queries | Elasticsearch read model | Full-text search, aggregation, analytics |
| Caching | None | Redis cache (sessions, hot data, rate limits) | Reduce DB load, sub-ms reads |
| Observability | Morgan HTTP logs | Structured logging + metrics + distributed tracing | Debugging across services |
4. System Design Deep Dives
4.1 Hierarchical IAM Authorization Engine
The challenge: 50+ routes with nuanced access rules. Simple role checks (if (user.role === 'admin')) would scatter authorization logic everywhere and make it impossible for admins to customize access without code changes.
The solution: A hierarchical permission system inspired by AWS IAM.
How the middleware works at request time:
Why this matters:
- Zero hardcoded role checks in business logic — all access controlled declaratively
- Admins can create custom roles at runtime by composing permissions
- Granting a parent permission cascades to all children — reduces configuration overhead
- Each module self-declares its permissions — the system is additive, not fragile
4.2 Transactional Points Economy
The challenge: Points are currency. Incorrect balances mean users see wrong totals, request prizes they can’t afford, or lose points. The system needs transactional integrity without a relational database.
The design:
Key design decisions:
- Transactions are immutable — append-only ledger, never modified or deleted
- Balances are derived views — computed from transactions, cached in a Balance document for fast reads
- Atomic updates via MongoDB
$inc— no read-modify-write race conditions - Reconciliation possible —
SUM(transactions.amount)for a (user, team) pair should always equalbalances.amount
At scale, this evolves to event sourcing:
4.3 Recursive Tree Structures in MongoDB
The challenge: Organisations have arbitrary-depth hierarchies. We need to efficiently query ancestors, descendants, siblings — and the structure needs to be updatable (reorg a team under a new parent).
The storage model: Recursive nested documents with team references.
{
"_id": "tree_001",
"r": "ministry_team_id",
"c": [
{
"r": "delegation_a_id",
"c": [
{ "r": "unit_1_id", "c": [] },
{ "r": "unit_2_id", "c": [] }
]
},
{
"r": "delegation_b_id",
"c": [
{
"r": "unit_3_id",
"c": [
{ "r": "sub_unit_3a_id", "c": [] }
]
}
]
}
]
}Trade-offs considered:
| Approach | Read | Write | Chosen? |
|---|---|---|---|
| Nested documents (current) | Fast subtree reads (single doc) | Full doc rewrite on restructure | Yes |
Adjacency list (parentId) | Slow subtree (recursive queries) | Fast single-node moves | No |
Materialised paths (/a/b/c) | Fast ancestor/descendant queries | Rewrite all descendant paths on move | Considered for scale |
| Nested sets (left/right) | Fastest reads | Expensive inserts (renumber) | Overkill for this domain |
The nested document approach was chosen because hierarchies are read-heavy (every team view resolves its tree), restructuring is rare, and the tree depth is bounded (typically 3–5 levels).
4.4 Multi-Factor Validation Pipeline
The design: Rather than hardcoding validation rules, I built a pluggable validation hook system — each deployment configures which validation types are active, and the auth pipeline executes them in sequence:
Why pluggable hooks: Different deployments needed different rules — one client required email-only verification, another needed phone + admin approval. The hook system made this a config change, not a code change.
5. Key Engineering Challenges
Challenge 1: Session Auth Across HTTP and WebSocket
Problem: Socket.io connections need to authenticate the same user as HTTP requests, but WebSockets don’t natively share Express sessions.
Solution: Custom Socket.io middleware that intercepts the handshake, parses the session cookie, loads the session from MongoDB, runs Passport deserialization, and populates socket.request.user — making the WebSocket connection aware of the same user context as REST requests.
Challenge 2: Approval Workflow State Machine
Problem: Actions (coaching/reinforcement) and prizes follow a suggest → approve/reject lifecycle. Multiple actors can interact with the same entity concurrently.
Solution: Designed an implicit state machine where the status field governs allowed transitions:
Only the Owner role can transition from pending. The controller validates both the status transition and the actor’s team role before executing — preventing race conditions where two owners approve the same action.
Challenge 3: Consistent Point Balances Without Transactions
Problem: MongoDB (pre-4.0 when this was designed) lacked multi-document transactions. A failed request mid-way could create a Transaction document but fail to update the Balance.
Solution:
- Transactions are created first (immutable record)
- Balance updated atomically via
$inc(not read-modify-write) - If the balance update fails, the transaction still exists as the source of truth
- A reconciliation query (
aggregate transactions → sum per user/team) can reconstruct any balance
Challenge 4: Recursive Tree Traversal Performance
Problem: Finding all descendants of a node in a recursive nested structure requires traversing the entire tree.
Solution: Built a Tree service with a flattened cache — on tree load, the nested structure is flattened into an indexed array of {ref, children[], parent} objects. This turns O(depth) recursive lookups into O(1) cache hits for findIndex(), children(), parent(), and siblings() operations.
Challenge 5: Cross-Platform Auth Token Strategy
Problem: The web dashboard uses server-side sessions (cookies), but the mobile app needs token-based auth that survives app restarts.
Solution: The API supports both modes from the same endpoint — POST /auth/signin?$jwt=true returns a JWT alongside the session. The mobile app stores the JWT in the device keychain (not AsyncStorage) and sets it as an Axios interceptor header. The web app relies on the session cookie. The IAM middleware resolves the user from either source.
6. Technology Stack
| Layer | Choices | Rationale |
|---|---|---|
| API Framework | Express.js | Mature, massive ecosystem, team familiarity. At scale would evaluate Fastify for raw throughput. |
| Database | MongoDB + Mongoose | Document model fits hierarchical data (trees, nested actors). Schema-less flexibility for rapid iteration. |
| Auth | Passport.js + sessions + optional JWT | Session-based for web (CSRF protection), JWT for mobile (stateless). Dual-mode from one endpoint. |
| Real-time | Socket.io + Redis adapter | Bi-directional events with fallback transports. Redis adapter enables multi-process pub/sub. |
| Payments | Stripe | Industry standard. Subscription lifecycle, SCA-ready, idempotent API. |
| Mobile | React Native | Single codebase for iOS/Android. Code sharing patterns with web (Formik, Yup, Axios, styled-components). |
| Styling | styled-components | Shared across web and mobile. Theming, dynamic styles, zero-class-name conflicts. |
| Validation | Ajv (backend) + Yup (frontend) | JSON Schema on the API for strict contracts. Yup on clients for UX-friendly form validation. |
| CI/CD | GitLab CI + SSH deploys | Simple, effective for the team size. Staging auto-deploys on develop, production requires manual approval. |
7. Retrospective: What I Would Do Differently
TypeScript Everywhere
The entire codebase is JavaScript. With hindsight, TypeScript would have prevented entire categories of bugs — especially around the IAM system (permission string typos), Mongoose model access (missing fields), and cross-module interfaces. The refactoring cost at this codebase size would be significant.
Event-Driven Architecture from Day One
Notifications, timeline entries, and balance updates are currently synchronous in the request path. A single action approval triggers 4-5 sequential writes. An event bus (SNS/SQS or even a simple Redis pub/sub) would decouple these:
Impact: Response time drops from ~500ms to ~100ms. Side effects become retryable. New projections (analytics, email digests) can subscribe without touching existing code.
API Gateway + JWT-Only Auth
Sessions work but don’t scale horizontally without sticky sessions or a shared session store. Stateless JWT auth with refresh token rotation would simplify horizontal scaling and prepare for a microservices split.
GraphQL for Mobile
The mobile app makes multiple sequential REST calls to assemble a single screen (team + members + balance + actions). A GraphQL layer would let the mobile client request exactly the data shape it needs in one round trip — critical on mobile networks.
Containerised Deployment (Kubernetes)
The current deploy script SSHs into servers and runs git pull + pm2 restart. This works for 1-2 servers but doesn’t scale. Docker images built in CI, pushed to a registry, deployed via Kubernetes would provide:
- Zero-downtime rolling deploys
- Automatic rollback on health check failure
- Horizontal pod autoscaling per service
- Environment parity (dev = staging = prod)
Observability
The system has basic Morgan HTTP logging. For production at scale, I would add:
- Structured JSON logging with correlation IDs across services
- Prometheus metrics — request latency, error rates, queue depths
- Distributed tracing (Jaeger/X-Ray) — trace a request from mobile → nginx → API → MongoDB
- Alerting — PagerDuty/Slack on error rate spikes, p99 latency thresholds
Testing Strategy
Current coverage is focused on API endpoint tests (Mocha) and mobile E2E (Detox). I would add:
- Contract tests between mobile/web clients and the API (Pact)
- Load testing (k6) to validate the points economy under concurrent writes
- Integration tests for the Stripe webhook flow end-to-end
8. Summary
| Dimension | What I Demonstrated |
|---|---|
| System Design | Multi-client architecture with shared auth, real-time layer, and 4 external integrations |
| Authorization Design | Hierarchical IAM engine — routes declare permissions, roles compose them, zero hardcoded checks |
| Data Modelling | Recursive tree structures, transactional points economy, pluggable validation pipeline |
| API Architecture | Self-registering modular system — 13 modules auto-discovered at boot with consistent patterns |
| Scalability Thinking | Designed a clear evolution path: event sourcing, service decomposition, CQRS, CDN, caching |
| Full-Stack Delivery | API + web dashboard + mobile app (iOS/Android) + infrastructure + CI/CD — end to end |
| Engineering Maturity | Honest retrospective on trade-offs, with concrete proposals for what scales and what doesn’t |