SyncMind is a full-stack, AI-assisted learning insights platform. It connects to a user's GitHub, YouTube, and Coursera activity, distills it into educational topics using a RAG-backed knowledge base, and recommends courses, repositories, videos, and jobs tailored to the user's interests and location.
Built with React, FastAPI, MySQL, Sentence-Transformers, KeyBERT, SerpApi (Google Jobs), and a Manifest V3 Chrome extension.
- What It Does
- Architecture
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- How the Recommender Works
- API Reference
- Chrome Extension
- Database
- Troubleshooting
- Credits
SyncMind helps a user understand and grow their own learning footprint:
- Sign in with Google — primary auth, also captures locale used for jobs.
- Connect GitHub via OAuth — reads starred repos, recent activity, profile.
- Connect YouTube via Google scope — reads recent watch / liked-video signals.
- Connect Coursera via the Chrome extension — scrapes My Learning when the extension is installed; falls back to cross-platform inference when it isn't.
- Get personalized recommendations for each platform: repos, videos, courses.
- See jobs that match your stack — derived from your activity and your IP-based location, served by Google Jobs (SerpApi).
- Dashboard with progress rings, connected-platform cards, and recommendation cards (including a "Jobs For You" section).
┌────────────────────────────────────────┐
│ React 18 SPA │
│ (CRA, Tailwind, Framer Motion) │
│ Landing · Dashboard · About pages │
└───────────────┬────────────────────────┘
│ REST + Session cookie
▼
┌──────────────────────────────────────────────────────────┐
│ FastAPI backend │
│ │
│ Auth (Google + GitHub OAuth) Recommenders │
│ Sessions (itsdangerous) ─ recommend-yt │
│ RAG Keyword Filter (MiniLM) ─ recommend-git │
│ KeyBERT keyword extraction ─ recommend-coursera │
│ SerpApi (Google Jobs) ─ get_jobs │
│ Idempotent migrations ─ /profile/location │
└──────────────────────┬───────────────────────────────────┘
│ mysql-connector-python
▼
┌──────────────────────────────┐
│ MySQL 8 (Docker) │
│ users, github/youtube/ │
│ coursera_recommendations, │
│ job_recommendations │
└──────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Chrome Extension (Manifest V3, optional) │
│ content script on coursera.org │
│ ─ scrapes __NEXT_DATA__, DOM, URL slugs │
│ ─ sends EXTRACTED payload to the React app │
└─────────────────────────────────────────────────┘
The backend is the source of truth. The Chrome extension is optional — if it's not installed the app gracefully derives Coursera recommendations from the user's GitHub + YouTube signals.
Frontend
- React 18 (Create React App, not Next.js)
- React Router DOM 6
- Tailwind CSS, Framer Motion, Lucide React, Recharts
Backend
- FastAPI + Uvicorn
itsdangerousSessionMiddlewaresentence-transformers(all-MiniLM-L6-v2) — retrieval embeddings for the educational RAG filterKeyBERT— keyphrase extractionSelenium(headless Chrome) — Coursera fallback scraping when neededmysql-connector-python— DB driverrequests— GitHub / YouTube / SerpApi clients
Storage
- MySQL 8 via Docker Compose (
./docker-compose.yml)
Extension
- Manifest V3 Chrome extension (
frontend/extension/)
SyncMind/
├── README.md ← you are here
├── docker-compose.yml ← MySQL service
├── backend/
│ ├── main.py ← FastAPI app, all endpoints
│ ├── auth.py ← Google + GitHub OAuth
│ ├── migrations.py ← idempotent schema migrations (startup)
│ ├── model.py ← RAG educational keyword filter
│ ├── requirements.txt
│ ├── .env.example
│ ├── app/
│ │ ├── knowledge_base.py ← compatibility wrapper → model.py
│ │ └── vectorizer.py
│ ├── utils/
│ │ └── data_loader.py ← classify_sentence wrapper → RAG
│ └── db/
│ └── init/001_schema.sql ← initial schema (fresh Docker volume)
└── frontend/
├── package.json ← CRA scripts (npm start / build)
├── tailwind.config.js
├── src/
│ ├── App.jsx ← router + global state
│ ├── pages/
│ │ ├── Landing.jsx ← OAuth + platform connect flows
│ │ ├── Dashboard.jsx ← recommendations + jobs section
│ │ └── About.jsx
│ └── components/
│ ├── RecommendationCard.jsx
│ ├── ConnectedPlatformCard.jsx
│ ├── PlatformCard.jsx
│ └── ...
└── extension/ ← Chrome MV3 extension
├── manifest.json
├── background.js
├── coursera-content.js ← __NEXT_DATA__ + DOM + slug scraper
└── landing-bridge.js ← page ↔ extension bridge
- Python 3.10+
- Node.js 18+ and npm
- Docker (for MySQL)
- A Google Cloud OAuth client (for Google + YouTube scope)
- A GitHub OAuth App
- A SerpApi key (for Jobs)
- A YouTube Data API v3 key
git clone https://github.com/kanishjn/SyncMind.git
cd SyncMinddocker compose up -d mysqlThe first-time boot will run backend/db/init/001_schema.sql automatically.
On subsequent boots, backend/migrations.py runs at FastAPI startup and
applies any missing columns/tables idempotently.
cd backend
# create .env from the template and fill in your secrets
cp .env.example .env # (Windows: copy .env.example .env)
# virtualenv
python -m venv venv
# macOS / Linux
source venv/bin/activate
# Windows
# venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --host 127.0.0.1 --port 8000Backend will print Database schema migrations applied (idempotent). on
successful startup.
cd frontend
npm install
npm startThe app runs at http://localhost:3000. The backend session cookie is set on
127.0.0.1:8000, so keep the OAuth callback URLs aligned (see env section).
See frontend/extension/README.md. It can be
side-loaded via chrome://extensions → Load unpacked pointing to that folder.
Without it the Coursera connect flow still works via the backend's
cross-platform fallback.
All backend secrets live in backend/.env. Use backend/.env.example as the
template. Summary:
| Variable | Required | Purpose |
|---|---|---|
YOUTUBE_API_KEY |
yes | YouTube Data API v3 |
GITHUB_TOKEN |
optional | Static fallback token for /search/repositories |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
yes | Google OAuth |
GOOGLE_REDIRECT_URI |
yes | http://127.0.0.1:8000/auth/google/callback |
GIT_CLIENT_ID / GIT_CLIENT_SECRET |
yes | GitHub OAuth App |
GITHUB_REDIRECT_URI |
yes | Must equal the GitHub OAuth App callback exactly (use 127.0.0.1, not localhost) |
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD |
yes | MySQL connection (defaults match docker-compose.yml) |
DATABASE_URL |
yes | SQLAlchemy-style URL (used by some helpers) |
SESSION_SECRET_KEY |
yes | Signs the session cookie |
SERPAPI_KEY |
yes | Google Jobs via SerpApi |
RAG_KB_PATH / EDUCATIONAL_KB_PATH |
optional | Path to the educational RAG knowledge base (.json, .jsonl, .csv, .txt, .md). Defaults to backend/app/educational_kb.jsonl when present |
RAG_SIMILARITY_THRESHOLD |
optional | Top retrieval score required for an educational keyword (default 0.45) |
RAG_TOP_K |
optional | Number of KB evidence documents retrieved per keyword (default 3) |
RAG_CACHE_PATH |
optional | Embedding cache path (default backend/app/rag_embeddings.npz) |
KB_THRESHOLD |
optional | Deprecated fallback for RAG_SIMILARITY_THRESHOLD |
⚠️ Important: GitHub is strict about the redirect URI —localhostand127.0.0.1are not interchangeable. Whichever you put in your GitHub OAuth App settings is whatGITHUB_REDIRECT_URImust be too.
The original prototype used TF-IDF + a Random Forest classifier to decide
whether a keyword was "educational." It was brittle, locked into a snapshot
of scikit-learn, and produced an InconsistentVersionWarning at startup.
It has been replaced with a RAG-based keyword filter in
backend/model.py:
- The knowledge base is loaded from
RAG_KB_PATH/EDUCATIONAL_KB_PATH, or frombackend/app/educational_kb.jsonlwhen that file exists. - Supported KB formats are JSON, JSONL, CSV, TXT, and Markdown. JSON/CSV rows
can use fields such as
title,topic,keywords,summary,description,content,body, ortext. - KB entries should describe educational topics or domains; this is retrieved evidence, not a mixed positive/negative classifier training set.
- Each KB document is embedded once with Sentence-Transformers
all-MiniLM-L6-v2and cached toRAG_CACHE_PATH. - At runtime, each KeyBERT phrase retrieves its top KB evidence by cosine
similarity. The phrase is accepted when the top retrieval score is at least
RAG_SIMILARITY_THRESHOLD(default0.45). utils/data_loader.classify_sentenceis still the stable wrapper for existing call sites, but it now delegates to the RAG filter.
Until a project-specific KB is provided, backend/model.py uses a small
built-in educational seed corpus so the app remains usable in development.
KeyBERT (extract_keywords) pulls keyphrases out of raw text (titles,
descriptions, slugs). Phrases are classified whole — splitting
"machine learning" into "machine" + "learning" was destroying the
context KeyBERT extracted.
- If the extension is installed and the user is signed in to Coursera, the
content script returns titles + slugs from
__NEXT_DATA__, the DOM (MutationObserver-watched), and URL slugs as a third evidence source. - If the extension is not present,
/recommend-courserais called with an empty history and the backend falls back to deriving keywords from the user's GitHub + YouTube recommendations (recommendCOURSERAcross-platform path).
_derive_job_query_for_user builds a job-search query that's specifically
shaped for Google Jobs:
- Pulls keywords from the user's GitHub + YouTube recommendations (Coursera is excluded because "Python course" makes a poor job query).
- Strips learning-oriented stopwords (
course,tutorial,lecture, …). - Maps technical concepts to role titles via
_DOMAIN_ROLE_MAP(e.g.tensorflow → machine learning engineer,react → frontend engineer). - Prefixes the dominant GitHub language if available
(e.g.
Python machine learning engineer).
Location comes from two sources, in order:
- Client-side IP geolocation (
ipapi.co) posted to/profile/location. - Google's
localefield captured during OAuth (fallback if no IP data).
Results are normalized and cached in the job_recommendations table.
All endpoints are mounted on the FastAPI app (backend/main.py) unless noted.
| Method | Path | Purpose |
|---|---|---|
| GET | /auth/google/login |
Start Google OAuth |
| GET | /auth/google/callback |
Google OAuth callback; sets session + persists locale |
| GET | /auth/claim |
Bind a pending Google session to a frontend tab |
| GET | /auth/github |
Start GitHub OAuth (scope URL-encoded) |
| GET | /auth/github/callback |
GitHub OAuth callback; stores token in session |
| GET | /auth/user |
Returns the current authenticated user |
| GET | /auth/test-session |
Session debug helper |
| Method | Path | Purpose |
|---|---|---|
| POST | /fetch |
Persist a user (after Google OAuth) |
| GET | /get_userid?email= |
Returns the internal user id |
| GET | /token?email= |
Returns the GitHub access token for a user |
| POST | /fetch_git |
Persist GitHub profile + recent activity |
| GET | /get_github_data?email= |
Returns stored GitHub profile |
| GET | /get_youtube_data?email= |
Returns stored YouTube profile |
| GET | /auth/status |
Per-platform connection flags |
| Method | Path | Purpose |
|---|---|---|
| POST | /recommend-yt |
Build + persist YouTube recommendations |
| POST | /recommend-git?token= |
Build + persist GitHub recommendations (prefers user OAuth token) |
| POST | /recommend-coursera |
Build + persist Coursera recommendations (cross-platform fallback when history is empty) |
| GET | /get_github_recommendations?email= |
Read GitHub recs |
| GET | /get_youtube_recommendations?email= |
Read YouTube recs |
| GET | /get_coursera_recommendations?email= |
Read Coursera recs |
| Method | Path | Purpose |
|---|---|---|
| POST | /profile/location |
Persist IP-derived location for the signed-in user |
| GET | /get_jobs?email= |
Build + cache jobs via SerpApi using interests + location |
| GET | /get_job_recommendations?email= |
Read cached job recommendations |
| Method | Path | Purpose |
|---|---|---|
| GET | /debug/session |
Dump the current session |
| GET | /debug/session/check |
Quick session sanity check |
The extension at frontend/extension/ is a Manifest V3 add-on that helps the
Coursera connect flow by reading the user's My Learning page from inside the
browser.
Key details:
- Manifest V3 requires
host_permissionsfor bothcoursera.organdlocalhost:3000/127.0.0.1:3000so the background worker can usechrome.tabs.*to message the React app. - The content script (
coursera-content.js) combines three evidence sources to survive Coursera's dynamic rendering:- Recursively walks the Next.js hydration payload (
__NEXT_DATA__). - DOM scraping under a
MutationObserver(Coursera lazy-renders). - Mines keywords from URL slugs (
/learn/machine-learning→machine learning).
- Recursively walks the Next.js hydration payload (
landing-bridge.jsannounces presence to the React app viapostMessage(EXTENSION_PRESENT) so the frontend can decide whether to wait for anEXTRACTEDpayload or skip straight to the fallback.
Load it manually via chrome://extensions → Developer mode → Load unpacked → frontend/extension. Bump the version in manifest.json to force reload
during development.
Schema is defined in backend/db/init/001_schema.sql and applied
automatically the first time the MySQL volume is created. For existing
volumes, backend/migrations.py runs at FastAPI startup and adds any
missing columns / tables in an idempotent way (INFORMATION_SCHEMA
checks before ALTER TABLE / CREATE TABLE).
Tables:
users— Google profile +locale,country,region,city,country_code,location_updated_at.github_recommendations,youtube_recommendations,coursera_recommendations— per-platform cached recs.job_recommendations— cached SerpApi job results with the query and location used to generate them.
To reset everything during development:
docker compose down -v # removes the volume so 001_schema.sql re-runs
docker compose up -d mysqlThe redirect_uri is not associated with this application (GitHub)
The redirect URI in backend/.env must match the GitHub OAuth App
exactly — including 127.0.0.1 vs localhost.
InconsistentVersionWarning from scikit-learn on startup
This was the old Random Forest model. It is no longer loaded; the RAG filter
in backend/model.py replaces it. Any old .pkl classifier/vectorizer files
are obsolete.
No Coursera keywords extracted Either install the extension and sign in to Coursera in that browser, or just hit Connect anyway — the backend will derive Coursera recs from your GitHub + YouTube signals.
Empty / irrelevant jobs
Ensure SERPAPI_KEY is set, connect GitHub + YouTube first (Coursera is
intentionally excluded from the job-query source pool), and that
/profile/location was called (the Dashboard does this automatically via
ipapi.co).