student-management-system-django-drf: A Django API Where Roles Shape the Whole App
A close look at a student management backend that uses permissions, scoped querysets, serializer validation, and signals to turn RBAC into a system design pattern.
- This repo treats role-based access control as an architectural pattern, not a login feature.
- Permissions, queryset scoping, serializer validation, and signals each guard a different boundary.
- The raw-password handoff is the most revealing design choice because it solves onboarding while preserving one-way hashing.
- The project reads like a deployable template because it combines Docker, PostgreSQL, Supabase support, OpenAPI docs, and tests.
The one thing this repo gets oddly right
The sharpest detail in this project is also the most controversial: when an admin creates a user, the system briefly carries the raw password long enough to email it once, then lets hashing do its job. That is not a cute trick. It is a real design answer to a real onboarding problem.
Most starter CRUD apps stop at "create user" and leave the rest to manual coordination. This repo goes further. It tries to make the first day of a new account feel automatic, while still keeping the password inaccessible after creation.
That choice sets the tone for the whole codebase. The interesting question is not whether the app has roles. It is how deeply those roles shape every layer that follows.
Roles are the data model
The custom user model is the foundation. Instead of treating admin, teacher, and student as labels applied later, the repo encodes them as a first-class role field on the user itself. That makes authorization part of the domain model, not a decorator on top of it.
class User(AbstractUser):
class Role(models.TextChoices):
ADMIN = "admin", "Admin"
TEACHER = "teacher", "Teacher"
STUDENT = "student", "Student"
role = models.CharField(
max_length=20,
choices=Role.choices,
default=Role.STUDENT,
)
That matters because downstream code can rely on the role being explicit and enumerable. Permissions become readable. Query filtering becomes deterministic. Serializer rules can branch on the same shared source of truth.
Permissions are only the first gate
The repo uses custom permission classes like `IsAdmin`, `IsTeacher`, and `IsStudent`, which makes the intent obvious at the view boundary. But the more important move is that permissions do not carry the whole burden. They only decide whether a request may continue.
| Layer | What it protects | What breaks if removed | What role it serves |
|---|---|---|---|
| Permission classes | Who may enter the endpoint | Unauthorized requests can reach protected views | First gate |
| Queryset scoping | Which objects exist for the requester | Users can see records that are not theirs | Data visibility |
| Serializer validation | Which fields may change | Legitimate updates can become privilege creep | Mutation control |
| Signals | What happens after save or delete | Views become bloated with email and notification logic | Side effects |
That division is the real architecture lesson. A secure DRF app does not rely on a single yes or no. It layers several smaller decisions, each with a narrow job.
Visibility changes by role
Once a request clears the permission class, the queryset decides what the user can actually see. That is where the app becomes contextual. Students see only their own records. Teachers see the records tied to their courses. Admins see the full dataset.
| Role | What they can see | What they can change | What they trigger |
|---|---|---|---|
| Student | Only their own enrollments and profile data | Only fields allowed by serializer rules | Their own notifications |
| Teacher | Their courses and related enrollments | Teacher-facing fields and course assignment context | Course and enrollment updates |
| Admin | Everything | User creation and broad management actions | Provisioning and account emails |
That is the hidden strength of the repo. Authorization is not binary. It is data-shaped. The API does not merely ask who you are. It asks which records should exist in your world.
Serializer validation stops privilege creep
The `UserProfileSerializer.validate()` method is where policy becomes stubborn. Even if a user reaches an update endpoint legitimately, the serializer can still reject changes to fields that should not move. In this codebase, that includes sensitive student attributes such as roll number and enrollment year.
class UserProfileSerializer(serializers.ModelSerializer):
def validate(self, attrs):
user = self.context["request"].user
if user.role == "student":
blocked = {"roll_number", "enrollment_year"}
if blocked.intersection(attrs.keys()):
raise serializers.ValidationError(
"Students cannot change academic identity fields."
)
return attrs
This is the kind of guardrail that saves a system from accidental overexposure. The request may be valid. The user may even be authenticated. But the change still has to satisfy the policy encoded in the serializer.
Signals keep the request path thin
The repo uses Django signals to send email notifications when courses or enrollments change. That keeps the views clean. The trade-off is discoverability. You gain a thinner request path, but you have to trace the signal chain to see the full behavior.
That is a good fit here because notifications are side effects, not the core transaction. The request handler can stay focused on permission, persistence, and validation. The email logic lives where it belongs, after the model event fires.
There is a familiar Django trade-off here. Signals are elegant when you want decoupling. They are less elegant when you want all business logic to be visible in one place. This repo accepts that cost in exchange for cleaner views.
This is built like a deployable template
The project does not read like a classroom exercise. Docker support, PostgreSQL, Supabase guidance, OpenAPI docs, JWT auth, and tests in each app all point in the same direction: this is meant to be run, extended, and shipped.
| Capability | Why it matters | What it signals about the repo |
|---|---|---|
| Docker and PostgreSQL | Fast local setup with a realistic database | The project expects real deployment constraints |
| Supabase support | Cloud-friendly database configuration | It is not locked to a toy local stack |
| OpenAPI docs | Readable API surface for consumers | The backend is meant for integration |
| Per-app tests | Confidence around auth and policy | The architecture is designed to be maintained |
That makes the repo useful as a pattern. You can imagine adding grades, attendance, or file uploads without rewriting the access model. The core idea is already stable: roles define what the system can see, edit, and emit.





