DuetStudio
Scheduling and payments for private music teachers.
Next.js · Supabase · Postgres row-level security · Stripe Connect

Parent pays then studio updated then teacher paid out
The problem
A private teacher is running a business without any of the tools for one. Lessons sit in a paper planner or a personal Google Calendar. Invoices get typed by hand each month, or forgotten. Payment arrives as cash, a check, or a Venmo handle in a text message. The software that does exist for this mostly assumes you are a twenty-seat music school, and prices like it.
What I built
- A calendar with one-off and recurring lessons, conflict detection, and reschedule requests from the student's side.
- Invoices built from the lessons that actually happened, sent by email and paid by card.
- A student portal that logs in by emailed link rather than a password, since a good number of the users are nine years old.
- Studios with several teachers: a shared calendar, roles, and payroll.
- Practice assignments and reminders, with a log so a teacher can see who has actually been practising between lessons.
Decisions and tradeoffs
Tuition goes to the teacher, not through me
Payments run through the teacher's own Stripe account, so the money never sits somewhere I control. Collecting it centrally and paying teachers out on a schedule would have been easier to build, but it means holding someone's income while it is in transit. For a teacher deciding whether to trust a new piece of software with their livelihood, that is the part that matters.
Removing a teacher waits until the end of the month
Adding a teacher to a studio takes effect immediately, because somebody just got hired and needs to work today. Removing one waits until the billing period ends, because you already paid for it. There is also a floor: the seat count cannot drop below the number of teachers actually working, so an admin tidying up the billing page cannot lock a colleague out of their own calendar.
Payroll remembers the rate it used
When a studio pays a teacher a percentage, the earnings record stores the rate it was worked out with, not just the result. Change someone's split next month and last month's report still says exactly what it said before. Numbers that quietly rewrite themselves are how people stop trusting a payroll page.
A retry that would have stranded a paying customer
Stripe retries webhooks, so the handler has to be safe to run twice. My first version recorded the event before processing it, which stops duplicates but creates something worse: if the handler then failed, the retry saw that record and skipped. Someone would be charged and never get what they paid for. Now a failure clears its own record, and if even that fails it logs loudly enough to be fixed by hand.
Some bugs are a missing line, not a wrong one
An account can own a personal studio and also belong to somebody else's. For a while half the queries respected which one you were looking at and half did not. Nothing errored and nothing leaked, so no test was ever going to catch it. I ended up writing a check that reads the codebase and fails the build when a query filters by teacher without also filtering by studio.
The hard part was never the features. It is that a teacher's income runs through this thing. A double charge, or a lesson that jumps three hours because of a time zone, is the kind of thing that makes someone stop trusting software with their business for good. Most of the decisions above are that worry, applied one screen at a time.