A platform that models an internship as a single lifecycle rather than a pile of paperwork: a company publishes an offer, a student applies, uploads the required documents, and ends with a scheduled defense that carries a jury and a grade — all as linked records instead of scattered files.
The problem & constraints
Internship administration usually lives in three places at once — a spreadsheet of who applied where, an inbox of attached reports, and a separate calendar of defense slots. Nothing reconciles them, so the real state of a student's file is whatever the last person to touch it remembers. The constraint was to represent the whole path in one schema, with each stage a record that can be queried, not an email thread.
Approach
- Five linked entities carry the domain: companies own internships, internships receive applications, applications lead to defenses, and documents attach to the user who uploaded them.
- Applications hold an explicit
statusenum (pending/accepted/rejected) rather than an inferred state, so "where is this file" always has a single answer in the database. - Role is chosen and validated at registration (
student/professor/admin), which lets the same routes serve different views without a separate account system per audience. - Resource controllers for the things you browse (internships, companies)
and dedicated endpoints for the state transitions (
/apply,/upload,/defense/schedule) — reads and moves through the lifecycle are deliberately different shapes.
Described in full:
- companies → internships (1..n)
- users — role: student, — professor, admin → documents — POST /upload — owner: user (1..n)
- internships → applications — POST /apply — status: pending, — accepted, rejected (1..n)
- applications — POST /apply — status: pending, — accepted, rejected → defenses — POST /defense/schedule — jury, grade (0..1)
Trade-offs made
Decision
A defense as its own table keyed on the application, not columns on the application: keeps schedule, jury, and grade out of the application row and lets a file legitimately exist without a defense yet, at the cost of one more join on the views that show a complete student record.
Decision
Role as a validated field on the user rather than a permissions package: the fastest honest path to three clearly-separated audiences, accepting that anything finer-grained than "student / professor / admin" would need to be rebuilt on a real authorization layer.
Results
A working path from a published internship offer through to a graded defense, where each step is a persisted record with an owner — the question "which students still owe documents" becomes a query rather than an audit of someone's inbox.
What I'd do differently
Store jury members as a real relation instead of a comma-separated string in a single column — the current shape can't answer "which defenses is this professor sitting on" without string matching. I'd also validate uploaded documents by MIME type and not just by size, since a 2 MB cap alone doesn't say anything about what's actually in the file.