SkillBridge AI — Test Prep Academy Platform
Multi-tenant AI learning platform for test prep academies. Socratic AI tutor that refuses to give answers, AI quiz generation from uploaded files, async pre-grading, weak-spot detection, and AI parent summaries. Full role-based platform across admin, instructor, student, and parent.
LIVE DEMOArchitecture
SkillBridge is a three-tier application — React SPA, FastAPI backend, PostgreSQL — with an AI provider layer that abstracts the LLM behind a single interface. The whole thing runs in Docker on a VPS with GitHub Actions auto-deploying on every push to main.
System diagram
┌─────────────────────────────────────────────────────────┐
│ Client Layer │
│ Admin · Instructor · Student · Parent │
│ (React + TypeScript + Vite + Tailwind) │
└────────────────────────────┬────────────────────────────┘
│ JWT bearer token
▼
┌─────────────────────────────────────────────────────────┐
│ Authentication Layer │
│ JWT decode → load User → role-based dependency │
│ require_role(ADMIN, INSTRUCTOR, STUDENT, PARENT) │
│ Every request scoped to the user's tenant (academy) │
└────────────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ FastAPI Router Layer (16) │
│ auth · academy · users · courses · enrollments │
│ assignments · submissions · quizzes · exams · practice │
│ tutor · study_plans · parents · gamification · ... │
└────────────────────────────┬────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ Service Layer (~35 modules) │
│ Business logic lives here, not in routers. Examples: │
│ tutor_service · quiz_generator · quiz_grader │
│ parent_summary_service · tutor_insights_service │
│ study_plan_service · gamification_service · ... │
└──────────┬───────────────────────────────┬──────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ PostgreSQL 16 │ │ AI Provider Layer │
│ │ │ (ai_provider.py) │
│ Multi-tenant: │ │ │
│ tenant_id on every │ │ AIProvider (ABC) │
│ domain row │ │ ├─ GroqProvider │
│ │ │ └─ AnthropicProvider │
│ JSONB for rubrics, │ │ │
│ study plans, tutor │ │ Swap via AI_PROVIDER │
│ message threads │ │ env var. Vision model │
│ │ │ auto-selected on images.│
└──────────────────────┘ └──────────────────────────┘
The AI provider abstraction
The single most important architectural decision. The entire app calls get_provider().send_message(...) and never knows whether Groq or Anthropic is on the other end. The contract is one abstract method:
class AIProvider(ABC):
@abstractmethod
def send_message(
self,
system_prompt: str,
messages: list[dict],
max_tokens: int = 800,
images: list[AttachedImage] | None = None,
) -> AIResponse: ...
Why it matters:
- Provider risk is isolated. Groq is fast and cheap for Llama, but if pricing changes or the service degrades, switching to Anthropic is a one-line env change —
AI_PROVIDER=anthropic— not a refactor across 35 service modules. - Vision is handled transparently. When a request includes images (a student photographs their handwritten work), the provider auto-selects a vision-capable model — Llama-4 Scout on Groq, native multimodal on Claude — and attaches the images to the last user message in whatever multipart format that provider expects. Calling code is unaware.
- Token accounting is uniform. Every response returns input/output token counts in the same
AIResponseshape regardless of provider, so usage tracking doesn't branch on provider.
See code/ai_provider.py for the full implementation.
Routers stay thin, services hold logic
The 16 routers are deliberately thin — they parse the request, check the role via a FastAPI dependency, and delegate to a service. All business logic lives in the ~35 service modules. This keeps the routers readable as a table of contents for the API and makes the logic unit-testable without spinning up HTTP.
Example: POST /quizzes/generate validates the request, confirms the caller is an instructor, and calls quiz_generator.generate_quiz(...). The router is ~15 lines; the generation logic, AI call, and JSON validation are all in the service.
Async pre-grading flow
Grading short-answer quiz questions with the AI is slow enough that doing it synchronously inside the submission request would make students wait. The flow is decoupled:
- Student submits a quiz → submission persisted immediately, student sees confirmation
- Pre-grading runs against each short-answer question via
quiz_grader.grade_short_answer(...), producing a suggested score + feedback per answer - The instructor opens the submission and sees the AI's suggestions pre-filled — they approve or override each score before publishing the final grade
The AI never publishes a grade on its own. It does the first pass; the instructor owns the decision. See ai-features.md.
Deployment
Docker Compose runs the backend, frontend, and Postgres. GitHub Actions builds and deploys to the VPS on every push to main. The live demo at skillbridge.robertjeanpierre.com is this exact pipeline.