The problem
A music school was running on a spreadsheet, a shared calendar, and a lot of text messages. Lessons moved, families had several children enrolled at different levels, and teachers were coordinating with parents through personal phone numbers.
They also ran a separate LearnDash site for course material, which meant every student had two accounts and remembered roughly one of them.
Constraints
- Minors are involved. Any messaging feature in a school context needs moderation and a record. That is not a nice-to-have.
- The buyer is not technical. The person approving the work runs a music school. Every decision had to be explainable without jargon, and the finished system had to be operable without me.
- An existing system had to stay. LearnDash was already in use and already paid for. Replacing it was not on the table.
What I built
A family, not a list of students
The first modelling decision was the one that mattered most. The obvious schema is a table of students. The correct schema for this client is a family account with several students attached, because that is how billing works, how scheduling conflicts arise, and how the parent thinks about it.
Getting this right at the start meant that “move all of the Hendersons to Thursday” was a query rather than a feature request six months later. This is why I model data before choosing anything else.
Moderated messaging
Staff and families can message each other inside the application. Messages pass through moderation rather than going direct, and everything is retained. No personal phone numbers, no channel outside the system, and a record if a question is ever raised.
Role-based access and audit logging
Teachers, administrators, and parents see different things and can do different things. Sensitive actions write an audit entry: who did it, what changed, when. For a school, being able to answer that question later is worth the small cost of writing the rows.
Single sign-on into LearnDash
Rather than replacing the learning platform, I bridged it. A student signs in once and moves into LearnDash without a second login. The application stays the identity provider, which keeps one source of truth for who is enrolled.
This was the most fiddly part of the build and the part the client noticed most, which is often how it goes.
Stack and why
FastAPI and PostgreSQL, with Pydantic models on every endpoint and SQLAlchemy underneath. React for the interface. The API is documented in OpenAPI, which matters more than usual here because the client may hire someone else to extend it, and that person should not have to ask me anything.
What I would do differently
I would have built the scheduling conflict rules as explicit, testable constraints from the first week rather than growing them out of bug reports. They ended up correct, but they were discovered rather than designed, and the tests came after the behaviour instead of describing it.