Acadify-Backend: The School System That Treats Attendance Like a Security Protocol
A clean-architecture Express stack built around rolling QR codes, Redis-backed token revocation, and role-aware academic workflows.
- Acadify-Backend treats attendance as a security problem, so its most interesting feature is a rolling QR flow that expires fast and depends on teacher and timetable context.
- The backend is built like a maintainable system, with clean separation between controllers, services, repositories, and middleware instead of a flat Express tutorial structure.
- JWT is not treated as permanently stateless here, because Redis-backed revocation gives logout real enforcement.
- Validation, role checks, and attendance writes all happen at the edge, which keeps the business logic focused on actual workflow rules.
Most school software starts with records. This one starts with a threat model. If a student can photograph a QR code and reuse it later, attendance becomes theater, so the backend turns the check-in into a short-lived, teacher-bound, schedule-validated security event.
The QR Code That Refuses to Be Shared
That is the project’s sharpest idea: attendance should fail if the wrong person, the wrong class, or the wrong minute is involved. The QR session is not a static artifact. It is a live token with an expiry window, a timetable check, and a teacher identity check baked into the flow.
Why Attendance Is a Security Problem
A QR code by itself solves nothing. It is easy to copy, easy to forward, and easy to abuse if it stays valid too long. The repository’s design acknowledges that weakness by tying the code to context, not just content.
| Simple QR attendance | Acadify-style attendance |
|---|---|
| Static code | Short-lived token |
| Anyone can reuse it | Only the right teacher and lecture context can mint it |
| Validation happens after the fact | Validation happens before the scan becomes real |
| Proxy attendance is easy | Proxy attendance is constrained by time and role |
That is why the attendance flow feels closer to authorization than to form submission. It is not collecting a presence mark. It is deciding whether a particular presence claim deserves to become a record.
The Clean Architecture Skeleton
The codebase is organized to keep that trust model intact. Models define the shape of data, repositories hide the database details, services hold the business rules, controllers speak HTTP, and middleware enforces boundaries before the system does any real work.
| Layer | Job | Why it matters |
|---|---|---|
| Models | Define Mongoose schemas | Keep data shape explicit |
| Repositories | Encapsulate data access | Decouple logic from MongoDB |
| Services | Run business rules | Keep workflows testable |
| Controllers | Handle HTTP input and output | Stay thin |
| Middleware | Check auth, roles, and validation | Stop bad requests early |
How the Request Actually Moves
A representative request starts at the controller. Validation middleware checks the payload, auth middleware checks the token and role, the service executes the business rule, and the repository layer talks to Mongoose. The point is not ceremony. The point is that each layer owns one kind of decision.
router.post(
'/attendance/mark',
authMiddleware,
requireRole(['student']),
validate(markAttendanceSchema),
asyncHandler(async (req, res) => {
const result = await attendanceService.mark(req.user.id, req.body.token);
res.status(200).json(result);
})
);
That shape matters because it prevents one big function from becoming a landfill. A request either passes each gate or it does not. By the time the service layer runs, the input has already been narrowed to something meaningful.
JWT, But Not Naively Stateless
The Redis blacklist pattern is the project’s quietest strong decision. Most small Express apps treat JWTs as fire-and-forget tokens. This codebase adds a revocation check, which means logout can actually invalidate the token instead of just pretending to.
| Basic JWT setup | JWT plus Redis blacklist |
|---|---|
| Token remains valid until expiry | Token can be revoked immediately |
| Logout is mostly client-side | Logout has server-side enforcement |
| State is impossible to inspect | State is tiny and targeted |
| Good for demos | Better for systems that care about control |
That is a good example of stateful statelessness. The token still behaves like a JWT, but the system refuses to treat it as untouchable.
Validation as a Boundary, Not a Cleanup Step
Joi validators sit at the edge, which is exactly where they belong. They keep malformed input from drifting deeper into the system, so the service layer can focus on real rules instead of defensive parsing.
| Without boundary validation | With boundary validation |
|---|---|
| Services must guard every field | Services receive shaped input |
| Errors appear late | Errors fail fast |
| Business logic gets noisy | Business logic stays readable |
| Bad requests consume more of the stack | Bad requests die near the door |
const markAttendanceSchema = Joi.object({
token: Joi.string().required()
});
export const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) return res.status(400).json({ message: error.message });
next();
};
That pattern keeps the project honest. A backend that expects discipline at the edge can keep its middle layers simpler, and simpler middle layers are easier to test, reason about, and evolve.
Where the AI Assistant Fits
The chatbot and document parsing pieces widen the system from record-keeping into assistance. That matters, but it is still a secondary story. The backend’s identity is defined first by its trust boundaries, then by its AI feature set.
In practice, that makes the AI layer feel like an add-on to a disciplined core rather than the product’s entire reason to exist. The architecture can absorb it because the core already separates concerns cleanly.
What This Project Is, and What It Is Not
| Typical tutorial Express app | Acadify-Backend |
|---|---|
| Controllers often reach straight into models | Repositories and services stay in between |
| JWT is treated as fully stateless | JWT is paired with Redis revocation |
| Validation is inconsistent | Validation is a first-class boundary |
| Attendance is just CRUD | Attendance is a time-limited authorization event |
This is not a finished enterprise AMS with every edge case solved. It is something more interesting for a technical reader: a backend that already thinks in systems, not just endpoints. That is why the QR flow stands out. It is the smallest feature that reveals the largest design intent.