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.

DomainB2B Team Performance & Gamification
RoleLead Engineer & System Architect
TeamSmall cross-functional team (2–3 engineers)
StackNode.js, Express, MongoDB, Redis, Socket.io, React, React Native, Stripe
Scale13 feature modules, 50+ API endpoints, 15+ collections, 4 external integrations

What I Built

A multi-client platform with three interfaces sharing one API:

ClientTechnologyAudiencePurpose
Mobile AppReact Native (iOS + Android)Team members, coaches, ownersDay-to-day operations — actions, prizes, timeline
Admin DashboardReact SPAPlatform administratorsClient management, subscriptions, billing
REST APINode.js / Express / MongoDBBoth clientsCentralised 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:

ConcernCurrentScaled VersionWhy
ComputeSingle PM2 processService-per-module behind load balancerIndependent scaling per traffic pattern
AuthServer-side sessions in MongoDBJWT + refresh tokens with Redis session storeStateless services, horizontal scaling
Points ledgerDirect writes to Balance collectionEvent-sourced transactions via message queueAudit trail, eventual consistency, replay
NotificationsSynchronous in request pathAsync via message queue (fan-out)Don’t block user requests on email/SMS/push
FilesGridFS in MongoDBS3 with presigned URLs + CDNOffload bandwidth, reduce API load
SearchMongoDB queriesElasticsearch read modelFull-text search, aggregation, analytics
CachingNoneRedis cache (sessions, hot data, rate limits)Reduce DB load, sub-ms reads
ObservabilityMorgan HTTP logsStructured logging + metrics + distributed tracingDebugging 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 possibleSUM(transactions.amount) for a (user, team) pair should always equal balances.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:

ApproachReadWriteChosen?
Nested documents (current)Fast subtree reads (single doc)Full doc rewrite on restructureYes
Adjacency list (parentId)Slow subtree (recursive queries)Fast single-node movesNo
Materialised paths (/a/b/c)Fast ancestor/descendant queriesRewrite all descendant paths on moveConsidered for scale
Nested sets (left/right)Fastest readsExpensive 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:

  1. Transactions are created first (immutable record)
  2. Balance updated atomically via $inc (not read-modify-write)
  3. If the balance update fails, the transaction still exists as the source of truth
  4. 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

LayerChoicesRationale
API FrameworkExpress.jsMature, massive ecosystem, team familiarity. At scale would evaluate Fastify for raw throughput.
DatabaseMongoDB + MongooseDocument model fits hierarchical data (trees, nested actors). Schema-less flexibility for rapid iteration.
AuthPassport.js + sessions + optional JWTSession-based for web (CSRF protection), JWT for mobile (stateless). Dual-mode from one endpoint.
Real-timeSocket.io + Redis adapterBi-directional events with fallback transports. Redis adapter enables multi-process pub/sub.
PaymentsStripeIndustry standard. Subscription lifecycle, SCA-ready, idempotent API.
MobileReact NativeSingle codebase for iOS/Android. Code sharing patterns with web (Formik, Yup, Axios, styled-components).
Stylingstyled-componentsShared across web and mobile. Theming, dynamic styles, zero-class-name conflicts.
ValidationAjv (backend) + Yup (frontend)JSON Schema on the API for strict contracts. Yup on clients for UX-friendly form validation.
CI/CDGitLab CI + SSH deploysSimple, 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

DimensionWhat I Demonstrated
System DesignMulti-client architecture with shared auth, real-time layer, and 4 external integrations
Authorization DesignHierarchical IAM engine — routes declare permissions, roles compose them, zero hardcoded checks
Data ModellingRecursive tree structures, transactional points economy, pluggable validation pipeline
API ArchitectureSelf-registering modular system — 13 modules auto-discovered at boot with consistent patterns
Scalability ThinkingDesigned a clear evolution path: event sourcing, service decomposition, CQRS, CDN, caching
Full-Stack DeliveryAPI + web dashboard + mobile app (iOS/Android) + infrastructure + CI/CD — end to end
Engineering MaturityHonest retrospective on trade-offs, with concrete proposals for what scales and what doesn’t