rpmjp/portfolio
rpmjp/projects/skillbridge/multi-tenancy.md
CompletedFebruary – April 2026

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 DEMO
Python 3.12FastAPIPostgreSQL 16SQLAlchemy 2.0ReactTypeScriptTailwindGroq / Llama
Languages
Python54.2%
TypeScript45.1%
CSS0.3%
JavaScript0.2%
Dockerfile0.1%
Mako0.1%
multi-tenancy.md

Multi-Tenancy

SkillBridge is multi-tenant: one deployment serves many independent test prep academies. Academy A must never see Academy B's students, courses, submissions, or AI tutoring activity. This is the part of the build with the highest stakes — a tenancy bug isn't a glitch, it's a data breach.


The model

Every academy is a tenant. Every domain row that could conceivably leak across tenants carries a tenant_id (the academy's ID). Users, courses, assignments, submissions, quizzes, tutor sessions, study plans, parent summaries — all of it is tenant-scoped.

academy (tenant)
  └── users (admin, instructor, student, parent)
        └── everything those users create or touch
              carries the academy's tenant_id

Two layers of isolation

Layer 1 — tenant scoping. Queries are scoped by the authenticated user's tenant_id. A student in Academy A issuing any request can only ever retrieve rows belonging to Academy A. The tenant is derived from the authenticated user, never from a request parameter the client could tamper with.

Layer 2 — role filtering within the tenant. Being in the right academy isn't enough. Within a single academy, a student must not see another student's submissions, and a parent must see only their own child's data. The backend returns role-filtered data:

  • Admin — full visibility into their academy (and only their academy)
  • Instructor — courses they teach, students enrolled in those courses, submissions to their assignments
  • Student — only their own enrollments, submissions, tutor sessions, study plan
  • Parent — only their linked child's grades, summaries, and study plan

The role check runs as a FastAPI dependency before any handler code executes. require_role(UserRole.INSTRUCTOR, UserRole.ADMIN) gates instructor endpoints; there's no path to a protected handler that skips the check. See code/auth_deps.py.


Why tenant_id on every row, not a schema-per-tenant

Two common multi-tenant patterns: a separate database schema per tenant, or a shared schema with a tenant_id discriminator. SkillBridge uses the shared-schema approach because:

  • Operational simplicity. One schema, one migration to run, one connection pool. Schema-per-tenant means N migrations and a connection-routing layer.
  • Onboarding a new academy is an INSERT, not a DDL operation. Creating a tenant doesn't require provisioning a schema or running migrations against a new namespace.
  • Cross-tenant analytics stay possible (for platform-level metrics) without federating queries across schemas.

The tradeoff is that isolation is enforced in application logic rather than by the database boundary, which raises the stakes on getting the scoping right — hence two layers and the dependency-level role gate.


The parent-child link

Parents are the trickiest role. A parent isn't enrolled in courses and doesn't submit work — they observe a specific student. The parent-child relationship is an explicit link, and the parent endpoints resolve the linked student first, then return only that student's data. A parent in Academy A linked to Student X cannot see Student Y, even though both students are in the same academy. The role filter is per-relationship, not just per-tenant.


What this looks like in the demo

The one-click demo roles show this directly. Open the Student role and you see one student's world. Open the Parent role and you see exactly one child's progress. Open Instructor and you see a teacher's roster-level view. Same deployment, same academy (the demo tenant), three completely different filtered views of the data — which is the whole point.