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. Thesrc/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/(Vitedist/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 bysrc/services/compatibilityEngine.ts— there is no/api/compareserver 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 completerequiresLifeDesign— 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.tsxrenders 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/notificationsto permanently remove all notifications for the user, then clears local state. - Notification links that reference a coach conversation include
?conv=<uuid>soMessages.tsxdeep-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
_idvalues are string UUIDs (generated viarandomUUID()on the server), not MongoDB ObjectIds. All server-sidefindOnecalls use atoDbId()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.ts — hasPaidAccess):
role === 'admin'→ always trueplan === 'coach'→ always trueplan === 'explorer'with noplanExpiresAt→ true (admin-granted permanent access)plan === 'explorer'with futureplanExpiresAt→ 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:
approved_pending_payment— admin pre-approved; user'scoachAppStatusset; payment gate shown in CoachPortalpayment_received— Stripe webhook confirmed payment; admin "Activate Coach" button appearsapproved— admin activated;coachesdoc created/reactivated;plan: 'coach'set on user;coachAppStatuscleared
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 toplan: 'explorer'deactivateOrgPlan(orgId)— clears org plan + batch-reverts all members toplan: 'free'- Members join via
/register?org=INVITE_CODE—orgId+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¶
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¶
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.tsx—useEffecton mount readsresponses/{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}.createdAtin 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 datasrc/data/questions.json— assessment questionssrc/data/scoring.json— scoring weightssrc/data/careerPaths.json— 3-path career data (acceleration / mastery / contribution)src/data/powerMoves.json— per-archetype behavioural leverssrc/data/actionPlans.json— growth action planssrc/data/manifestos.json— path manifestossrc/data/swotTemplates.json— SWOT prompts for Strategic Audit step