Skip to content

Architecture & Technical Design

Last updated: April 2026 (revised — stale file cleanup: agents.js and firebase.js removed, /api/compare entry corrected, selfInquiry model path fixed)

Stack

Layer Technology
Frontend React 19 + TypeScript (strict mode) + Vite 6
Styling Tailwind CSS 4
Animation motion/react 12
Icons lucide-react
Charts Chart.js 4 + react-chartjs-2, Recharts
Routing react-router-dom v7
Auth VPS JWT (issued by /api/auth/* endpoints; stored in localStorage)
Backend API Express.js on Hostinger VPS (port 5002, PM2, Node 20)
Database MongoDB (personality_db) on VPS
Payments Stripe Checkout → VPS webhook handler (/api/stripe/*)
AI Google Gemini (@google/genai)
PDF Export html2canvas-pro + jsPDF

Note: Firebase has been completely removed. Auth is handled by the VPS (/api/auth/* endpoints issuing 30-day JWTs). All data is stored in MongoDB. The src/api/ folder holds the VPS HTTP client (client.ts), JWT auth helpers (auth.ts), a localStorage shim (config.ts), and the MongoDB data layer (db.ts).


Infrastructure

Browser
  └─ HTTPS → mind-meditate.com (Nginx, Let's Encrypt SSL)
       ├─ /api/*    → proxy → Express :5002  (API + Stripe webhook)
       ├─ /health   → proxy → Express :5002
       └─ /*        → static files from /opt/personality-app/public/  (Vite dist)
  • VPS: Hostinger, IP 76.13.211.100
  • Static files: /opt/personality-app/public/ (Vite dist/ uploaded via SCP)
  • Backend app dir: /opt/personality-app/
  • Process manager: PM2 (personality-app, id 2)
  • MongoDB: mongodb://localhost:27017/personality_db

Application Structure

src/
  pages/          — Full-page route components (filename = PascalCase menu name + "Page")
    StartHerePage.tsx           — menu: Start Here       (/questionnaire)
    DesignMyLifePage.tsx        — menu: Design My Life   (/life-design)
    MindBodyPage.tsx            — menu: Mind & Body      (/wellness-profile)
    MoneyPlanPage.tsx           — menu: Money Plan       (/financial-plan)
    MyReportPage.tsx            — menu: My Report        (/report)
    DailyGrowthPage.tsx         — menu: Daily Growth     (/growth-loop)
    SelfMasteryPage.tsx         — menu: Self Mastery     (/self-inquiry)
    PeopleBlueprintPage.tsx     — menu: People Blueprint (/corporate)
    RelationshipLensPage.tsx    — menu: Relationship Lens (/compare)
    Dashboard.tsx, Profile.tsx, ArchetypeReveal.tsx
    CoachPortal.tsx, CoachApply.tsx, Coaching.tsx, Messages.tsx
    OrgDashboard.tsx, SharedView.tsx, TeamReport.tsx, Workshops.tsx
    LandingPage.tsx, Login.tsx, Register.tsx, CompleteProfile.tsx
    Pricing.tsx, PaymentSuccess.tsx, TermsOfService.tsx, PrivacyPolicy.tsx
    lifeDesign/   — Design My Life step components (6 steps) — sub-folder name unchanged
    growthLoop/   — Daily Growth tab components (4 tabs) — sub-folder name unchanged
    financialPlan/— Money Plan step components — sub-folder name unchanged
    wellnessProfile/— Mind & Body step components — sub-folder name unchanged
    admin/        — AdminDashboard
  components/     — Shared UI components (Layout, ProtectedRoute, JourneyProgressStrip)
  contexts/       — UserProfileContext (polling-based auth + profile feed)
  services/       — Pure logic services (ikigaiEngine, insightEngine, careerInsightEngine, aiService, paymentService)
  api/            — auth.ts = VPS auth calls; client.ts = VPS HTTP client + JWT localStorage helpers; config.ts = localStorage shim (auth.currentUser); db.ts = all MongoDB API calls
  utils/          — Scoring engine + journey gate rules + gamification logic
  data/           — Static JSON data (framework, questions, scoring, etc.)
  types/          — TypeScript interfaces

server/src/
  server.js       — Express entry point
  middleware/     — auth.js (JWT verification via `jwt.verify(token, JWT_SECRET)`), adminMiddleware (DB role check), orgAdminMiddleware, rateLimiter.js
  routes/         — One file per resource:
    auth.js, users.js, responses.js, coaches.js, coachRequests.js,
    notifications.js, organisations.js, cohorts.js, teams.js,
    shareLinks.js, promoCodes.js, coachApplications.js,
    conversations.js, workshops.js, reportFeedback.js,
    stripe.js, ai.js, admin.js
    (/api/agents is served by the standalone agents-service on port 5003 — agents.js removed from personality-app)
  utils/          — agentCommissions.js (commission tier logic, shared by stripe.js)

API Design

All backend routes are prefixed /api/. Auth is validated via VPS-issued JWT in the Authorization: Bearer <token> header (authMiddleware in middleware/auth.js).

Key route groups:

Prefix Description
/api/auth Auth endpoints (no token required). POST /register (create account, returns JWT), POST /login (verify credentials, returns JWT), POST /forgot-password (sends SMTP reset email), POST /reset-password (sets new password via token), GET /me (own user doc, requires token)
/api/users User profiles. GET /me (own), PUT /me (own update — plan/payment fields excluded), GET /:uid (self or admin), PUT /:uid (admin — general field update), PUT /:uid/assign-coach (org admin)
/api/ai Gemini AI endpoints for chat and refinement features
/api/admin Admin-only atomic operations. POST /change-plan — atomically updates plan, planExpiresAt, coachAppStatus, and the coaches collection in one DB round-trip. All plan/payment state changes must go through this endpoint — never call /api/users/:uid directly for plan changes.
/api/responses Assessment responses
/api/coaches Coach profiles. GET supports ?email= and ?referralCode= filters — must be applied as DB filters, not ignored. POST supports { upsert: true } for create-or-reactivate by email (used internally by /api/admin/change-plan). See access-control.md §8 for the April 2026 filter bug.
/api/coach-requests Coach–user connection requests
/api/organisations Orgs; POST /:id/activate-plan, POST /:id/deactivate-plan
/api/cohorts Coach cohorts (CRUD)
/api/teams Teams; POST /join, POST /leave, GET /:code/members
/api/stripe Checkout, webhook, verify-session; fires agent commission to agents-service (port 5003) after payment
/api/conversations Messaging. GET / — list user's conversations; POST / — create conversation (uses randomUUID() string _id); GET /:id/messages — paginated message list; POST /:id/messages — send message. Frontend polls conversations every 5 s and messages every 3 s.
/api/notifications User notifications. GET / — list unread/recent. PUT /mark-read — mark all read. DELETE / — delete all notifications for current user (used by "Clear all" button in Layout).
/api/share-links Report share tokens
/api/promo-codes Promo code lookup + redeem
/health Health check

Note: Compatibility comparison (RelationshipLensPage.tsx, /compare) is computed entirely client-side by src/services/compatibilityEngine.ts — there is no /api/compare server route.


5-Stage User Journey

Stage 1 — Discover  →  StartHerePage → MyReportPage
Stage 2 — Know Self →  MindBodyPage
Stage 3 — Design    →  DesignMyLifePage (6 steps)
Stage 4 — Do        →  DailyGrowthPage
Stage 5 — Fund      →  MoneyPlanPage

Journey gates enforced by ProtectedRoute.tsx:

  • requiresDiscovery — blocks Stage 2/3 until assessment is complete
  • requiresLifeDesign — blocks Stage 3 until Future Studio is complete

Routing

All routes defined in App.tsx:

Path Component Gate
/ LandingPage Public
/login, /register Login, Register Public
/pricing Pricing Public
/terms, /privacy TermsOfService, PrivacyPolicy Public
/share/:token SharedView Public (token-gated)
/payment/success PaymentSuccess Public
/complete-profile CompleteProfile Auth
/dashboard Dashboard Auth
/questionnaire StartHerePage Auth
/report MyReportPage Auth
/profile Profile Auth
/messages Messages Auth
/coach-portal CoachPortal Auth
/org-dashboard OrgDashboard Auth
/archetype-reveal ArchetypeReveal Auth
/life-design DesignMyLifePage requiresDiscovery + requiresPayment
/growth-loop DailyGrowthPage requiresDiscovery + requiresLifeDesign + requiresPayment
/financial-plan MoneyPlanPage requiresDiscovery + requiresPayment
/wellness-profile MindBodyPage requiresDiscovery + requiresPayment
/coach-apply CoachApply requiresDiscovery + requiresPayment
/coaching Coaching requiresDiscovery + requiresPayment
/workshops Workshops requiresDiscovery
/team TeamReport requiresDiscovery + requiresPayment
/compare RelationshipLensPage requiresDiscovery + requiresLifeDesign + requiresPayment (ProtectedRoute) — also has inline GateScreen for richer progress display
/reset-password ResetPassword Public (token in query string)
/corporate PeopleBlueprintPage canAccessCorporate (requiresDiscovery + requiresLifeDesign + requiresPayment)
/self-inquiry SelfMasteryPage canAccessSelfInquiry (requiresDiscovery + requiresPayment)
/admin AdminDashboard Admin role only

Layout & Navigation

  • Layout.tsx renders the sidebar navigation and (on mobile) a top bar.
  • Header hidden on desktop for authenticated inner pages — the top header bar is only shown on mobile/tablet to avoid double navigation chrome.
  • Collapsible sidebar on inner pages (Report, Wellness, LifeDesign, Financial): users can collapse the sidebar to gain horizontal space. Collapse state is toggled per-session.
  • Notification bell in the sidebar and top bar:
  • Unread count badge.
  • Clicking opens a fixed-position dropdown panel (avoids overflow/clipping issues).
  • "Mark all read" button marks every notification read server-side and clears the badge.
  • "Clear all" button calls DELETE /api/notifications to permanently remove all notifications for the user, then clears local state.
  • Notification links that reference a coach conversation include ?conv=<uuid> so Messages.tsx deep-links to the correct thread.

State Management

No global state library. Each page manages its own state via useState / useEffect.

The single shared global state is UserProfileContext (src/contexts/UserProfileContext.tsx), which exposes { user, profile, loading, profileLoading, journeyStage } by polling GET /api/users/me every 30 seconds. All route guards, nav lock icons, and auth-aware UI consume useUserProfile() from this context.

user is type AuthUser { uid: string; email: string } — not a Firebase User object. Auth state is restored from localStorage key personality_auth (written by client.ts:setStoredAuth) and kept live via a personality_auth_change CustomEvent listener. Previously Firebase onAuthStateChanged was used — replaced in April 2026.

Messages (Messages.tsx)

  • Polling-based real-time messaging between coach and client.
  • Conversations listed in a left panel; clicking one opens the thread.
  • Active conversation set from ?conv=<id> URL param on load; if no param is present, the first conversation is auto-selected.
  • Conversations polled every 5 seconds; messages polled every 3 seconds.
  • Conversation _id values are string UUIDs (generated via randomUUID() on the server), not MongoDB ObjectIds. All server-side findOne calls use a toDbId() helper that handles both formats for backward compatibility.

Scoring Engine (src/utils/scoring.ts)

Pure functions only — no side effects, no Firebase calls.

  • calculateResult(answers) — core scoring function. Do not modify without full regression testing.
  • smartOrderQuestions() — interleaves categories and alternates difficulty for engagement.
  • isAssessmentStable(answers, questions) — early-completion signal.

Services Layer

Service File Purpose
Ikigai Engine ikigaiEngine.ts Derives IkigaiProfile from archetype + force. Returns love / goodAt / worldNeed / paidFor / theme / statement.
Insight Engine insightEngine.ts Generates 4 InsightPanel objects (self, direction, decision, evolution) from archetype + force data. Fully rule-based — no AI calls.
Career Insight Engine careerInsightEngine.ts Produces CareerEnhancement — personalised career context panels from profile fields.
AI Service aiService.ts Wraps Gemini API — powers ReportChat.tsx (streaming report Q&A). Also wired for future AI refinement buttons.
Compatibility Engine compatibilityEngine.ts Computes CompatibilityResult from two archetype+force pairs. Outputs score, summary, strengths, challenges, dosList, dontsList, approachGuide, traitsToWatchOut, conflictFlashpoints.
Self-Inquiry Service selfInquiryService.ts Defines the 5 inquiry phases (PHASES, PHASE_META), SIX_PATTERNS, PERSONAL_PRINCIPLES, and prompt builders (buildSelfInquirySystemPrompt, buildDataPhaseContext, buildRealityPhaseContext, buildActionSystemPrompt). Consumed by SelfMasteryPage.tsx.
Corporate Engine corporateEngine.ts Computes CorporateGuideResult per relationship (computeCorporateGuide) and GroupCompatibilityResult for the full org chart (computeTeamIntelligence). Also exports generateTeamNarrative, buildTeamCoachSystemPrompt, buildTeamSuggestedQuestions, buildSharedFromUserProfile, CORPORATE_ARCHETYPES, ARCHETYPE_DESCRIPTIONS.
Payment Service paymentService.ts Thin wrapper around VPS API (POST /api/stripe/create-checkout-session). Accepts plan, userId, referralCode?, promoCodeId?.

Gamification Utility (src/utils/gamification.ts)

Pure functions — no Firebase calls, no side effects. Consumed by pages and UserProfileContext.

Export Purpose
XP_REWARDS Map of action key → XP value (e.g. ASSESSMENT_COMPLETE: 500)
BADGE_DEFINITIONS Map of badge ID → { label, icon, description } for all 9 badges
computeLevel(xp) Returns 'Stardust' \| 'Comet' \| 'Nova' \| 'Supernova' based on thresholds
levelProgress(xp) Returns 0–100 percentage within the current level band
nextLevelThreshold(xp) Returns { level, threshold } for the next level, or null at Supernova
computeStreak(habits, today?) Iterates habit completedDates backwards to compute consecutive-day count
computeNewBadges(profile, ctx?) Returns array of newly earned badge IDs not already in profile.badges
applyXp(profile, action) Returns updated { xp, level, badges } — does not write to DB
computeBackfill(profile) One-time retroactive calculation for existing users: derives XP + level + badges + streak from completed stages

Key Type Definitions (src/types/framework.ts)

Type Purpose
SectionDetail { meaning, realLife, strength, growth } — used by all archetype section cards
IkigaiFormState Form state for the Ikigai step. Fields: passion / profession / mission / vocation / statement. Maps to IkigaiProfile as: passion↔love, profession↔goodAt, mission↔worldNeed, vocation↔paidFor
IkigaiProfile Engine output from ikigaiEngine.ts. Fields: love / goodAt / worldNeed / paidFor / theme / statement
InsightReport Four InsightPanel objects returned by insightEngine.ts
CareerEnhancement Personalised career context object from careerInsightEngine.ts

MongoDB Data Model

users collection — UserProfile

Key fields:

uid, name, email, role ('user' | 'admin')
discoveryCompleted, lifeDesignCompleted
plan ('free' | 'explorer' | 'coach'), planExpiresAt (ISO string | null), stripeCustomerId
coachAppStatus ('approved_pending_payment' | 'payment_received' | undefined)
referredBy (coachId), referralCode (coaches only), teamId
orgId (null = individual), orgRole ('member' | 'admin'), assignedCoachId
lifeDesign: { vision, mission, selectedPath, ikigai, pillars, swot, strategicHabits, capabilityGap }
  pillars: { faith, health, family, career, finances, growth,
             anchors: { ...same keys },
             timeBound: { ...same keys } }
growthLoop: { habits, weeklyReflections, monthlyReviews, weeklyChallenge?: { weekKey, completed } }
shareToken (for public share link)
selfInquiry?: { sessions: SelfInquirySession[] }  — nested under selfInquiry.sessions; array of completed Self Mastery sessions (phase answers + AI conversation)
corporateRelationships?: CorporateRelationship[]  — array of added workplace relationships (name, archetype, relationshipType)

Gamification fields (all optional — backfilled automatically on first load):
xp?: number                                 — cumulative XP
level?: 'Stardust'|'Comet'|'Nova'|'Supernova'
badges?: string[]                           — earned badge IDs
streakCount?: number                        — current consecutive habit days
lastStreakDate?: string                     — ISO date 'yyyy-MM-dd'
streakFreezes?: number                      — 0 or 1 freeze tokens

Plan access logic (src/utils/journeyGates.tshasPaidAccess):

  • role === 'admin' → always true
  • plan === 'coach' → always true
  • plan === 'explorer' with no planExpiresAt → true (admin-granted permanent access)
  • plan === 'explorer' with future planExpiresAt → true (active paid or org subscription)
  • All other cases → false

responses collection — Assessment Response

One document per user (retakes overwrite). Key fields:

uid, answers, archetype, secondaryArchetype, force, scores,
confidenceScore, energyOrientation, profileType, createdAt, assessmentHistory[]

createdAt is used by the 6-month retake cooldown enforced in DiscoveryPage.tsx.

coaches + coachApplications collections — Coaching

coaches/{id}:          name, email, title, bio, archetypes, modules, location, sessionType,
                       priceMin, priceMax, referralCode, status ('active' | 'inactive'), createdAt
coachApplications/{id}: name, email, archetype, message,
                       status ('pending' | 'approved_pending_payment' | 'payment_received' |
                               'approved' | 'rejected')
coachRequests/{id}:    userId, coachId, status ('pending'|'proposed'|'accepted'|
                       'awaiting_payment'|'payment_submitted'|'active'), proposal fields
commissions/{id}:      referralCode, coachId, clientUid, amount, createdAt
processedWebhooks/{sessionId}: processedAt (idempotency guard for Stripe events)

Coach application status lifecycle:

pending → approved_pending_payment → payment_received → approved
        ↘ rejected (at any stage)
  • approved_pending_payment — admin pre-approved; user's coachAppStatus set; payment gate shown in CoachPortal
  • payment_received — Stripe webhook confirmed payment; admin "Activate Coach" button appears
  • approved — admin activated; coaches doc created/reactivated; plan: 'coach' set on user; coachAppStatus cleared

organisations collection — Corporate Accounts

organisations/{id}: name, inviteCode (8-char unique), coachIds[],
                    plan ('explorer' | undefined), planExpiresAt (ISO string | null),
                    seatLimit (number | null), createdAt
  • activateOrgPlan(orgId, expiresAt?) — writes plan to org doc + batch-updates all member user docs to plan: 'explorer'
  • deactivateOrgPlan(orgId) — clears org plan + batch-reverts all members to plan: 'free'
  • Members join via /register?org=INVITE_CODEorgId + orgRole: 'member' auto-assigned on registration

workshops collection — Workshop Events

workshops/{id}: title, facilitator, date, time, description, archetypes, format, cost, registrationUrl, status

teams collection — Team Reports

teams/{teamId}: members[], archetypeMap{}, createdAt

promoCodes collection — Promo Codes (admin-managed)

code, type ('free_access' | 'percent_off' | 'fixed_off'), value, plan,
maxUses (null = unlimited), usedCount, usedBy[], expiresAt, note, active, createdBy

reportFeedback collection — Post-Report Feedback

uid, rating ('very_helpful' | 'somewhat_helpful' | 'not_helpful'), note, submittedAt

One document per user — submitting again overwrites the previous entry.


Assessment Retake Cooldown

Users are locked from retaking the assessment for 6 months after their first submission.

  • Check location: DiscoveryPage.tsxuseEffect on mount reads responses/{uid}.createdAt
  • Lock duration: SIX_MONTHS_MS = 6 * 30 * 24 * 60 * 60 * 1000
  • Locked UI: Shows the last assessment date, next unlock date, and a countdown in days
  • Manual admin override: Edit responses/{uid}.createdAt in MongoDB (via Admin > Users in AdminDashboard or direct MongoDB access) to any date 6+ months ago

Why 6 months: Personality archetypes are stable over months and years. Research shows meaningful shifts take at least 6 months of new life experience to register. Short re-tests mostly reflect mood, not real change.


Payment Flow (Stripe)

Explorer Plan (RM 99/year)

Client (Pricing.tsx)
  ↓ createCheckoutSession({ plan: 'explorer', userId, referralCode?, promoCodeId? })
VPS API: POST /api/stripe/create-checkout-session
  ↓ creates Stripe Checkout session (optionally applies coupon for discount codes)
  ↓ returns { url }
Browser → Stripe-hosted checkout page
  ↓ on success: redirects to /payment/success?session_id=...
Stripe Webhook (checkout.session.completed)
  → verifies signature, writes plan: 'explorer', planExpiresAt (+1 year) to users collection
  → increments usedCount + usedBy[] on promoCodes (if promo used)
  → records commission if referralCode present
Stripe Webhook (invoice.payment_succeeded, subscription_cycle)
  → extends planExpiresAt by 1 year
Stripe Webhook (customer.subscription.deleted)
  → sets plan: 'free', removes planExpiresAt

Coach Plan (RM 599 year 1 → RM 199/year renewal)

Coach plan uses a Stripe Subscription Schedule with two phases (RM 599 → RM 199). The webhook does not activate the portal directly — it requires admin review first.

CoachPortal (payment gate — only shown when coachAppStatus === 'approved_pending_payment')
  ↓ createCheckoutSession({ plan: 'coach', userId, ... })
VPS API: POST /api/stripe/create-checkout-session
  ↓ creates Stripe Checkout session at STRIPE_COACH_FIRST_PRICE_ID
  ↓ returns { url }
Browser → Stripe-hosted checkout
Stripe Webhook (checkout.session.completed)
  → sets coachAppStatus: 'payment_received' on users document
  → updates coachApplications document → status: 'payment_received'
  → creates SubscriptionSchedule (phase 1: RM 599, phase 2: RM 199 recurring)
  → does NOT set plan: 'coach' directly — admin must activate
Admin clicks "Activate Coach" in AdminDashboard → Coaches tab
  → creates coaches document, sets plan: 'coach', clears coachAppStatus
Stripe Webhook (customer.subscription.deleted)
  → sets plan: 'free', clears coachAppStatus, deactivates coaches document

PDF Export (Report.tsx)

Uses html2canvas-pro + jsPDF. Each [data-pdf-section] element in the report renders as its own PDF page. Responsive Tailwind classes are inlined at capture time to ensure multi-column layouts render correctly regardless of viewport width.


Data-Driven UI

No report or dashboard content is hardcoded. All text is pulled from:

  • src/data/framework.json — archetype sections, leadership profiles, learning profiles, financial data
  • src/data/questions.json — assessment questions
  • src/data/scoring.json — scoring weights
  • src/data/careerPaths.json — 3-path career data (acceleration / mastery / contribution)
  • src/data/powerMoves.json — per-archetype behavioural levers
  • src/data/actionPlans.json — growth action plans
  • src/data/manifestos.json — path manifestos
  • src/data/swotTemplates.json — SWOT prompts for Strategic Audit step