Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Unistep

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


Features

Scholarship discovery

  • 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

Journey wizard (/journey)

  • 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

Professor finder (/professors)

  • AI professor matching with hiring-confidence scoring
  • Filters for actively hiring / likely hiring professors

Email drafting (/email-drafter)

  • Chat-style gathering flow before generating a draft
  • Revise drafts via natural-language feedback
  • Mark as sent, follow-up drafting, mailto integration

Application tracker (/applications)

  • 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}

Notification center (/notifications)

  • 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)

Platform

  • 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

Repository structure

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

Architecture

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]
Loading

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.


User routes (frontend)

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


Scholarship discovery flow

  1. User submits the discovery form on /discovery.
  2. React calls POST /api/v1/users/me/discovery/search (JWT).
  3. Spring merges the logged-in profile with form overrides and calls FastAPI at APP_DISCOVERY_FASTAPI_URL with HMAC headers.
  4. FastAPI runs: query planner → web search → extraction → ranking → quality guard.
  5. 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

Docker services

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

Prerequisites

Docker (recommended): Docker + Docker Compose

Manual dev: Node.js 18+, Java 17+, Maven, MySQL 8+, Redis, Python 3.12+


Environment configuration

Copy .env.example to .env and fill in secrets locally. Never commit .env.

Required

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:5173 when running via Docker (nginx proxies /api → app). Use http://localhost:9090 only for manual frontend dev against a bare Spring instance.

Fresh local database (Flyway)

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=false

These 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 frontend

Discovery (Spring → FastAPI)

APP_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=130000

Scholarship AI service

LLM_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>

Quick start (Docker)

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 ps

Endpoints

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 frontend

Rebuild after code changes

docker compose up -d --build app frontend          # backend + UI
docker compose up -d --build scholarship-ai app    # AI + backend

Quick start (manual)

Backend

cd saas-springboot
# Configure application.properties / env; MySQL + Redis must be running
mvn spring-boot:run

Frontend

cd saas-reactjs
npm install && npm run dev
# Set VITE_API_BASE_URL=http://localhost:9090 in .env.local

Scholarship AI

cd 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 --reload

Point Spring at APP_DISCOVERY_FASTAPI_URL=http://localhost:8081/v1/discovery/search.


Development commands

# 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 tests

API highlights

Base path: /api/v1 (JWT unless noted)

Auth

Method Path Description
POST /auth/register Register
POST /auth/login Login
POST /auth/refresh-token Refresh JWT
GET /auth/oauth/google/authorize Google OAuth

Discovery & journey

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

Outreach & tracker

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

Notifications

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

Profile & scholarships

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

FastAPI (internal, HMAC)

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


Database migrations

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/


Security notes

  • Never commit .env, API keys, or CLOUDFLARED_TUNNEL_TOKEN.
  • Use strong APP_JWT_SECRET (64+ chars) and APP_DISCOVERY_AUTH_SHARED_SECRET in production.
  • Rotate secrets if they were ever exposed.
  • FastAPI is not exposed publicly in default compose; only Spring calls it on the Docker network.

Troubleshooting

Backend crash-loop on fresh DB (Flyway)

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 frontend

MySQL access denied after password change

The 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

Frontend cannot reach API

  • Docker: set VITE_API_BASE_URL=http://localhost:5173 (not 9090).
  • Check APP_CORS_ALLOWED_ORIGINS includes your frontend origin.
  • Confirm backend: curl http://localhost:9090/actuator/health

502 on discovery / professor search

  • Check docker compose logs app scholarship-ai.
  • Verify APP_DISCOVERY_FASTAPI_URL=http://scholarship-ai:8081/v1/discovery/search inside Docker.
  • Confirm HMAC secret matches on app and scholarship-ai.
  • Set GROQ_API_KEY (or another LLM key) and increase APP_DISCOVERY_HTTP_READ_TIMEOUT_MS if searches are slow.

Discovery returns few results

  • Rebuild FastAPI: docker compose up -d --build scholarship-ai
  • Set SEARCH_PROVIDER=serper with SERPER_API_KEY, or use duckduckgo without a key.
  • Check logs for pipeline counts (queries, evidence, validated, final).

Frontend Docker build fails (TypeScript)

Rebuild after fixing TS errors:

docker compose up -d --build frontend

Deployment

See 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=true on baselined production DB
  • APP_DISCOVERY_FASTAPI_URL on internal Docker network URL
  • Strong HMAC shared secret + LLM/search API keys
  • Cloudflare tunnel → http://frontend:5173

Contributing

  • 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__/.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages