Unistep is a study-abroad scholarship platform that helps students discover funding, find professors and universities, draft outreach emails, and track applications end to end.
| Layer | Stack |
|---|---|
| Frontend | React 19 + TypeScript + Vite + Tailwind |
| Backend | Spring Boot 3 + JWT/OAuth + MySQL + Redis |
| AI service | FastAPI + LangGraph (scholarship search, professor matching, email drafting) |
| Infra | Docker Compose + optional Cloudflare Tunnel |
Production site: https://www.unistep.digital
- AI-powered web research pipeline (query planning → search → extract → rank → validate)
- Save and manage scholarships with client-side search/filter
- Optional fallback to legacy n8n webhook if FastAPI is unavailable
- 4-step guided flow: countries → universities → professors → next actions
- Persists wizard state server-side (
/api/v1/discovery/journey-state) - Global search bar for cross-step lookups
- AI professor matching with hiring-confidence scoring
- Filters for actively hiring / likely hiring professors
- Chat-style gathering flow before generating a draft
- Revise drafts via natural-language feedback
- Mark as sent, follow-up drafting, mailto integration
- Table view — Excel-style sortable/filterable pipeline
- Kanban view — drag columns by outreach status
- Detail panel — program, deadline, IELTS, documents checklist, reply status
- Full record updates via
PATCH /api/v1/outreach/{id}
- Scheduled alerts: follow-ups (7/14/21 days), deadline warnings, IELTS reminders, stale applications, positive replies, weekly summaries
- Snooze reminders (
PATCH /api/v1/notifications/{id}/snooze)
- JWT auth + Google OAuth, email verification, password reset
- Academic profile, document uploads (transcript, resume)
- Admin dashboard (users, sessions, app settings)
- Swagger/OpenAPI, Prometheus metrics, rate limiting
| Path | Purpose |
|---|---|
saas-reactjs/ |
React SPA — discovery, journey, tracker, email drafter, admin |
saas-springboot/ |
Spring Boot API — auth, profiles, discovery proxy, outreach, notifications |
scholarship-ai-service/ |
FastAPI — scholarship search, professor/university agents, email agents |
docker-compose.yml |
Full stack: db, redis, app, scholarship-ai, frontend, cloudflared |
.env.example |
Environment template for Docker Compose |
DEPLOYMENT_VPS_GUIDE.md |
Production VPS deployment walkthrough |
OAUTH_GOOGLE_SETUP.md |
Google OAuth configuration |
flowchart LR
Browser[Browser]
Nginx[Frontend nginx :5173]
Spring[Spring Boot :9090]
FastAPI[Scholarship AI :8081]
MySQL[(MySQL)]
Redis[(Redis)]
Webhook[Optional n8n webhook]
Browser --> Nginx
Nginx -->|"/api/*"| Spring
Nginx -->|"/uploads/*"| Spring
Spring --> MySQL
Spring --> Redis
Spring -->|HMAC| FastAPI
Spring -.->|fallback| Webhook
FastAPI --> LLM[LLM APIs]
FastAPI --> Search[Search APIs]
The frontend nginx container proxies /api and /uploads to Spring. FastAPI is internal-only on the Docker network; Spring calls it with HMAC service auth.
| Route | Page |
|---|---|
/ |
Landing |
/login, /register |
Auth |
/dashboard |
User dashboard |
/discovery |
Scholarship discovery search |
/scholarships |
Saved scholarships |
/journey |
4-step journey wizard |
/professors |
Professor finder |
/email-drafter |
AI email drafting |
/applications |
Application tracker (table + kanban) |
/notifications |
Notification center |
/profile |
Academic profile & documents |
/admin/* |
Admin panel |
Public guides: /guides/scholarship-application-checklist, /guides/how-to-find-study-abroad-scholarships, /guides/country-deadline-planning
- User submits the discovery form on
/discovery. - React calls
POST /api/v1/users/me/discovery/search(JWT). - Spring merges the logged-in profile with form overrides and calls FastAPI at
APP_DISCOVERY_FASTAPI_URLwith HMAC headers. - FastAPI runs: query planner → web search → extraction → ranking → quality guard.
- Results return to the frontend; user can save via
POST /api/v1/users/me/saved-scholarships.
If FastAPI fails and APP_DISCOVERY_FALLBACK_WEBHOOK_ENABLED=true, Spring retries against APP_DISCOVERY_WEBHOOK_URL.
| Layer | Key file |
|---|---|
| Frontend | saas-reactjs/src/pages/users/DiscoveryPage.tsx |
| Spring client | saas-springboot/.../Services/Impl/DiscoveryServiceImpl.java |
| FastAPI pipeline | scholarship-ai-service/app/services/discovery_engine.py |
| Service | Container | Host port | Role |
|---|---|---|---|
db |
unistep-spring-db | 3307 → 3306 | MySQL 8 |
redis |
unistep-spring-redis | 6379 | Cache / rate limiting |
app |
unistep-spring-app | 9090 | Spring Boot API |
scholarship-ai |
unistep-spring-scholarship-ai | internal 8081 | FastAPI agents |
frontend |
unistep-spring-frontend | 5173 | React + nginx |
cloudflared |
unistep-spring-cloudflared | — | Optional Cloudflare tunnel |
Docker (recommended): Docker + Docker Compose
Manual dev: Node.js 18+, Java 17+, Maven, MySQL 8+, Redis, Python 3.12+
Copy .env.example to .env and fill in secrets locally. Never commit .env.
APP_JWT_SECRET=<random-string-64-chars-minimum-for-HS512>
MYSQL_DATABASE=saas-app
MYSQL_ROOT_PASSWORD=<password>
DATABASE_HOST=db
DATABASE_PORT=3306
REDIS_HOST=redis
REDIS_PORT=6379
SPRING_PROFILES_ACTIVE=prod
# Docker + nginx: same origin so /api is proxied (see saas-reactjs/nginx.conf)
VITE_API_BASE_URL=http://localhost:5173
APP_CORS_ALLOWED_ORIGINS=http://localhost:5173
# At least one LLM key for AI features
GROQ_API_KEY=<your-key>
APP_DISCOVERY_AUTH_SHARED_SECRET=<shared-secret>JWT secret: Must be at least 64 characters for HS512 or Spring will fail to start.
VITE_API_BASE_URL: Use
http://localhost:5173when running via Docker (nginx proxies/api→ app). Usehttp://localhost:9090only for manual frontend dev against a bare Spring instance.
Flyway migrations V1+ assume a pre-existing user table (created historically by Hibernate). On a brand-new database:
SPRING_JPA_HIBERNATE_DDL_AUTO=update
SPRING_FLYWAY_ENABLED=falseThese are the Docker Compose defaults. Hibernate creates/updates the full schema from JPA entities. Enable Flyway (SPRING_FLYWAY_ENABLED=true) only on databases that were already baselined (existing production deploys).
To reset a corrupted local DB:
docker compose down
docker volume rm unistep-spring_db_data
docker compose up -d --build db redis scholarship-ai app frontendAPP_AI_BASE_URL=http://scholarship-ai:8081
APP_DISCOVERY_FASTAPI_URL=http://scholarship-ai:8081/v1/discovery/search
APP_DISCOVERY_FALLBACK_WEBHOOK_ENABLED=true
APP_DISCOVERY_AUTH_MODE=hmac
APP_DISCOVERY_AUTH_KEY_ID=spring-backend
APP_DISCOVERY_AUTH_SHARED_SECRET=<shared-secret>
APP_DISCOVERY_HTTP_READ_TIMEOUT_MS=130000LLM_PRIMARY_PROVIDER=groq
LLM_FALLBACK_PROVIDERS=groq,gemini
GROQ_MODEL=llama-3.3-70b-versatile
DISCOVERY_MAX_RESULTS=15
SEARCH_PROVIDER=serper # or duckduckgo, tavily
SERPER_API_KEY=<optional>
TAVILY_API_KEY=<optional>From the repo root:
cp .env.example .env
# Edit .env: APP_JWT_SECRET (64+ chars), GROQ_API_KEY, APP_DISCOVERY_AUTH_SHARED_SECRET
# Local dev — skip cloudflared (no tunnel token needed)
docker compose up -d --build db redis scholarship-ai app frontend
docker compose psEndpoints
| URL | Description |
|---|---|
| http://localhost:5173 | Frontend (nginx) |
| http://localhost:5173/applications | Application tracker |
| http://localhost:9090/api/v1 | API (direct) |
| http://localhost:9090/swagger-ui/index.html | Swagger UI |
| http://localhost:9090/actuator/health | Health check |
After a fresh DB reset, register a new account at http://localhost:5173/register — there are no seeded users.
Logs
docker compose logs -f app scholarship-ai frontendRebuild after code changes
docker compose up -d --build app frontend # backend + UI
docker compose up -d --build scholarship-ai app # AI + backendcd saas-springboot
# Configure application.properties / env; MySQL + Redis must be running
mvn spring-boot:runcd saas-reactjs
npm install && npm run dev
# Set VITE_API_BASE_URL=http://localhost:9090 in .env.localcd scholarship-ai-service
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DISCOVERY_AUTH_MODE=none # local only; use HMAC in Docker/prod
uvicorn app.main:app --host 0.0.0.0 --port 8081 --reloadPoint Spring at APP_DISCOVERY_FASTAPI_URL=http://localhost:8081/v1/discovery/search.
# Frontend — dev, test, lint
cd saas-reactjs && npm run dev && npm run test && npm run lint
# Backend — unit tests
cd saas-springboot && mvn test
# Scholarship AI — pytest
cd scholarship-ai-service && python3 -m pytest testsBase path: /api/v1 (JWT unless noted)
| Method | Path | Description |
|---|---|---|
| POST | /auth/register |
Register |
| POST | /auth/login |
Login |
| POST | /auth/refresh-token |
Refresh JWT |
| GET | /auth/oauth/google/authorize |
Google OAuth |
| Method | Path | Description |
|---|---|---|
| POST | /users/me/discovery/search |
Scholarship discovery |
| POST | /discovery/professors |
Start professor search job (202) |
| GET | /discovery/professors/jobs/{jobId} |
Poll professor search job |
| POST | /discovery/universities |
University search |
| POST | /discovery/global-search |
Cross-entity search |
| GET/PUT | /discovery/journey-state |
Journey wizard persistence |
| Method | Path | Description |
|---|---|---|
| POST | /outreach/draft-email |
AI email draft |
| POST | /outreach/revise-email |
Revise draft via chat |
| POST | /outreach |
Create outreach record |
| GET | /outreach |
List pipeline |
| GET | /outreach/{id} |
Get record |
| PATCH | /outreach/{id} |
Full tracker field update |
| PATCH | /outreach/{id}/status |
Update status only |
| POST | /outreach/{id}/draft-followup |
AI follow-up draft |
| DELETE | /outreach/{id} |
Delete record |
| Method | Path | Description |
|---|---|---|
| GET | /notifications |
List notifications |
| GET | /notifications/unread-count |
Unread count |
| PATCH | /notifications/{id}/read |
Mark read |
| PATCH | /notifications/{id}/snooze |
Snooze reminder |
| Method | Path | Description |
|---|---|---|
| GET/PUT | /users/me/profile |
Academic profile |
| GET/POST/DELETE | /users/me/saved-scholarships |
Saved scholarships |
| POST | /users/me/documents/* |
Upload transcript/resume |
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| POST | /v1/discovery/search |
Scholarship pipeline |
| POST | /v1/agents/professors/jobs |
Start async professor ReAct search (202 + job id) |
| GET | /v1/agents/professors/jobs/{jobId} |
Poll professor search job status/results |
| POST | /v1/agents/universities |
University matching |
| POST | /v1/agents/global-search |
Global search |
| POST | /v1/agents/draft-email |
Email draft |
| POST | /v1/agents/revise-email |
Email revise |
| POST | /v1/agents/draft-followup |
Follow-up draft |
Full API docs: http://localhost:9090/swagger-ui/index.html
Flyway SQL migrations live in saas-springboot/src/main/resources/db/migration/:
| Version | File | Purpose |
|---|---|---|
| V1 | phase6_new_tables |
academic_profiles, professor_outreach, notifications |
| V2 | user_public_id |
UUID public IDs |
| V3 | migrate_profile_data |
Profile data migration |
| V4 | uuid_columns_varchar |
UUID column types |
| V5 | plan2_hiring_fields |
Hiring confidence on outreach |
| V6 | tracker_application_fields |
Tracker fields + notification snooze |
Manual one-off scripts: saas-springboot/src/main/resources/db/manual/
- Never commit
.env, API keys, orCLOUDFLARED_TUNNEL_TOKEN. - Use strong
APP_JWT_SECRET(64+ chars) andAPP_DISCOVERY_AUTH_SHARED_SECRETin production. - Rotate secrets if they were ever exposed.
- FastAPI is not exposed publicly in default compose; only Spring calls it on the Docker network.
Detected failed migration to version 1 ... Failed to open the referenced table 'user'
Set SPRING_FLYWAY_ENABLED=false and SPRING_JPA_HIBERNATE_DDL_AUTO=update, then wipe the volume:
docker compose down && docker volume rm unistep-spring_db_data
docker compose up -d --build db redis scholarship-ai app frontendThe MySQL volume persists the original root password. Either revert .env to match or remove the volume (data loss):
docker compose down && docker volume rm unistep-spring_db_data- Docker: set
VITE_API_BASE_URL=http://localhost:5173(not 9090). - Check
APP_CORS_ALLOWED_ORIGINSincludes your frontend origin. - Confirm backend:
curl http://localhost:9090/actuator/health
- Check
docker compose logs app scholarship-ai. - Verify
APP_DISCOVERY_FASTAPI_URL=http://scholarship-ai:8081/v1/discovery/searchinside Docker. - Confirm HMAC secret matches on
appandscholarship-ai. - Set
GROQ_API_KEY(or another LLM key) and increaseAPP_DISCOVERY_HTTP_READ_TIMEOUT_MSif searches are slow.
- Rebuild FastAPI:
docker compose up -d --build scholarship-ai - Set
SEARCH_PROVIDER=serperwithSERPER_API_KEY, or useduckduckgowithout a key. - Check logs for pipeline counts (queries, evidence, validated, final).
Rebuild after fixing TS errors:
docker compose up -d --build frontendSee DEPLOYMENT_VPS_GUIDE.md and .github/workflows/deploy.yml (self-hosted runner + UNISTEP_DOTENV secret).
Production checklist:
- All secrets via host env or CI secret — not in git
VITE_API_BASE_URL=https://www.unistep.digital(nginx proxies/api)SPRING_FLYWAY_ENABLED=trueon baselined production DBAPP_DISCOVERY_FASTAPI_URLon internal Docker network URL- Strong HMAC shared secret + LLM/search API keys
- Cloudflare tunnel →
http://frontend:5173
- Match existing code style; keep diffs focused.
- Run tests before pushing (
mvn test,npm run test,pytest). - Do not commit
.env,node_modules/, or__pycache__/.