
Walkthrough
Video coming soon
Overview
SOLACE is an emotionally intelligent AI wellness companion. It’s not a medical app and it’s not a chatbot — it’s built to feel like a personal health detective with a warm, human soul: something that helps you notice the quiet, hard-to-name things (low energy, brain fog, mood swings, “just off”) and surfaces honest, non-clinical patterns in your own data, without ever diagnosing or prescribing.
It’s a solo portfolio project, built in public phases, and it’s in active development: Phases 0–4 are complete and Phase 5 is in progress.
Why I built it
I wanted to explore how AI could be used for wellness without turning into another generic chatbot. The idea came from noticing how difficult it can be to connect things like sleep, stress, energy, mood, and lifestyle into a bigger picture, so I built SOLACE around finding those patterns while keeping the experience non-clinical and human.
Who it’s for
People who feel that something is “off” but cannot easily explain why, especially students and young professionals dealing with stress, inconsistent routines, and generic wellness advice.
My role
I designed and built SOLACE end-to-end, including the product flow, architecture, backend/ML separation, data model, AI pipeline, and frontend experience. The hand-drawn visual language was also intentionally designed as part of the product identity, with customized UI components rather than using a generic template.
What it does


Daily check-ins
Mood, energy, sleep, water, sunlight, stress, symptoms, food — in under two minutes.

Rule engine + Gemini insight cards
A rule engine maps symptom patterns to possible nutrient and lifestyle flags (iron, vitamin D, magnesium, B12, vitamin C, sleep debt, dehydration, sedentary behavior, sustained stress), sourced from peer-reviewed nutrition research.
Gemini turns those flags into warm, specific, hedged language — never “you have a deficiency,” always “there might be a pattern here.”

A personal pattern model
scikit-learn, trained per-user and stateless. It learns your data after 14+ check-ins and surfaces an honest next-day mood/energy prediction — banded into plain language (“steadier,” “brighter”) rather than raw numbers, and hidden entirely when the model isn’t confident.
What it deliberately doesn’t do
No diagnoses. No medical claims. No supplement or affiliate pushing. No dark patterns.
The journal will never feed any AI/ML layer — that boundary is architectural, not a setting.
Architecture
Three independently deployed services:
Client
Next.js
Vercel
API
Express + TypeScript API
Railway
Data
Supabase Postgres
Postgres + Auth, via Prisma
External
Google Gemini Flash
Service
FastAPI ML service
Railway, stateless
- Express + TypeScript API → Supabase Postgres — Prisma
- Express + TypeScript API → Google Gemini Flash — insight cards
- Express + TypeScript API → FastAPI ML service — GET /api/patterns, decoupled
- Sync
- Async
- Auth
- The frontend never talks to the database, Gemini, or the ML service directly — everything routes through the backend.
GET /api/patternsis decoupled from the main dashboard load and has a 4-second timeout: if the ML service is slow, asleep, or fails, the prediction card is silently hidden rather than blocking the dashboard.- The journal (Phase 5) is encrypted in the browser using Web Crypto: the key never leaves the client, and the backend stores only ciphertext.
Stack
- Frontend
- Next.js 16 (App Router), TypeScript, Tailwind CSS 4, Framer Motion, custom hand-drawn UI kit
- Backend
- Express 5, TypeScript, Prisma, Zod
- ML service
- Python 3.12, FastAPI, scikit-learn, pandas, numpy, scipy
- Auth & DB
- Supabase (Postgres + Auth)
- AI
- Google Gemini Flash
- Deploy
- Vercel (frontend), Railway (backend + ML), Supabase Cloud
Key decisions
A separate Python ML service
- Why
- Python has a much stronger ML ecosystem through libraries like scikit-learn, while TypeScript is better suited to the application’s API and business logic. Keeping it separate also gives the ML layer its own deployment and scaling boundary, while preventing the frontend from ever accessing the ML service directly.
- Trade-off
- It adds another service to develop, deploy, monitor, and communicate with, but the separation gives me cleaner responsibilities and makes the architecture easier to evolve.
A stateless model, retrained per request
- Why
- The backend sends the user’s complete check-in history to the ML service, which trains the Gradient Boosting models in memory for that request and immediately returns the prediction. This avoids storing model files on Railway’s ephemeral filesystem and keeps the service horizontally scalable.
- Trade-off
- The model has to retrain for every prediction request, so there is some additional CPU/time cost compared with loading a persisted model. With the current small per-user datasets, the training is lightweight enough that the simplicity and reliability are worth it.
Rule engine decides, Gemini writes
- Why
- The rule engine is responsible for the deterministic wellness signals/flags, while Gemini turns those signals into warm, human-readable insight copy. This keeps the important decision logic deterministic and testable instead of asking an LLM to independently decide whether a user has a particular signal.
- Trade-off
- It requires maintaining two layers instead of letting one AI model do everything, but it gives much better control, testability, and consistency.
Gemini Flash
- Why
- SOLACE needs fast, lightweight text generation rather than a large reasoning model. It is used mainly for synthesizing already-structured signals into concise, warm insight cards, so a faster model with low inference cost was a better fit. The implementation also includes rate limiting, caching/storage of generated insights, and fallback copy when Gemini is unavailable.
Supabase with Prisma
- Why
- Supabase gives me managed PostgreSQL plus authentication, while Prisma gives the backend a type-safe ORM and migration workflow. Supabase handles infrastructure/auth while the Express backend retains ownership of database access, instead of allowing the frontend to directly query application data.
A 14-check-in gate and confidence checks
- Why
- The model is learning an individual’s patterns rather than using a large population dataset, so below fourteen check-ins there simply isn’t enough personal history to justify a prediction. After training, confidence combines sample count, cross-validation stability, correlation agreement, and model variance; the prediction is also hidden on negative R², degenerate output variance, or unstable correlation patterns across bootstrap resamples.
Challenges
Training a model on very little data per user
The biggest ML challenge was making a personalized model useful without pretending that a small dataset is highly reliable. I used conservative Gradient Boosting hyperparameters, a minimum 14-check-in gate, recency weighting, correlation analysis, permutation-importance cross-checking, bootstrap stability checks, and confidence/sanity gates. If those checks do not produce a sufficiently reliable signal, SOLACE simply hides the prediction instead of forcing an answer.
Keeping Gemini from sounding like it was diagnosing someone
Because SOLACE is a wellness product rather than a medical application, I treated the AI output as a constrained generation problem rather than trusting the model’s wording by default. Gemini is given structured signals from the rule engine, prompts are designed around non-clinical language, and generated text passes through a tone validator with 19 banned phrases. There is also fallback copy and a “not medical advice” disclaimer on insight cards.
Deferring emotion classification to Phase 7
The Hugging Face emotion model was deferred because the free inference infrastructure was unreliable and the model’s PyTorch memory requirements were too high for Railway’s 512 MB free tier. Rather than compromising the core product around an unreliable dependency, I kept Phase 4 focused on structured check-in data and moved free-text emotion classification to Phase 7.
Quality and safety
- Automated tests
- 149
- Rule-engine rules
- 9
- Banned phrases
- 19
Testing
- 89 backend tests (Vitest) cover the rule engine, the Gemini tone validator and the ML client.
- 60 ML-service tests (pytest) cover feature preparation and the model’s training, sanity, stability and confidence gates.
Rule engine
Nine rules: five deficiency-related rules (iron, vitamin D, magnesium, B12, vitamin C), three lifestyle rules (sleep debt, dehydration, sedentary behaviour) and one sustained-stress rule. The rules are based on peer-reviewed nutritional science, with their sources documented in the project.
Security and safety
- Supabase authentication with session verification on API routes.
- Zod validation at API boundaries, and Prisma parameterized queries.
- Rate limiting, sanitized markdown, and environment-variable separation.
- Gemini is kept behind the backend rather than being exposed directly to the frontend.
Progress
Phase 0 — Monorepo, deploy pipeline, Supabase (complete)
Phase 1 — Auth + animated onboarding (complete)
Phase 2 — Daily check-in + dashboard (complete)
Phase 3 — Rule engine + Gemini insights (complete)
Phase 4 — Personal ML pattern model (complete)
Phase 5 — Encrypted journalIn progress
Phase 6 — Women’s wellness section (not started)
Phase 7 — Polish, accessibility, docs, deploy hardening (not started)
Phase 5 is the private encrypted journal: its encrypted backend is done, and the journal interface is next. The deferred emotion-classification work is planned for Phase 7.
What I learned
Adding AI to a product is less about calling an LLM and more about designing the system around it — deterministic logic, validation, safety constraints, fallbacks, and clear boundaries matter just as much as the model itself.
How to design a distributed application where different services have deliberately different responsibilities instead of putting everything into one backend.
Personalization with small datasets requires being comfortable saying “there isn’t enough evidence yet” instead of forcing the system to produce an answer.