Homeschool Reports
Your homeschool report, written in as little as 12 hours.
Problem
Every year, home-educating families in Queensland have to write a compliant annual report. It is stressful and slow, and parents lose weekends to paperwork instead of teaching their kids.
Solution
Homeschool Reports takes that off their plate. Parents send a short submission and their work samples, and I write the compliant annual report, often within twelve hours, with free revisions until the Home Education Unit accepts it.
What it is
A service for Queensland home-educating families that turns a short submission and work samples into a compliant annual home-education report.
Design process
General research
Every year, home educating families in Queensland have to write a compliant annual report. I studied the actual Queensland Home Education procedure and the Act, and I watched parents lose weekends to it.
Market validation
I built a service catalogue from the real requirement, listing every document a family needs, and I priced it by how much stress each one takes off the parent. That told me exactly what to offer and what to charge.
Ideation
I designed a funnel where the parent answers a few questions, pays by how fast they need it, and hands me their work samples, and I write the report. No AI drafting, a real report written by hand.
Testing
I instrumented the funnel with analytics and conversion events so I could see where people dropped off, and I ran a security audit of the payments, the admin, and the data handling.
Iteration
I acted on the real drop off data. I removed a which state step that only ever had one answer and was costing sign ups, and I moved the payment before the long questions.
Build
I built the questionnaire engine, the Stripe checkout with three speed tiers, a due date clock that pauses overnight for the fastest tier, an admin dashboard, a leads system, a free resource studio, and two way email with customers.
Test
A data integrity audit led to atomic writes, an admin login that fails closed and stores only a hashed password, real deletion with a retention period, and verified inbound email.
Release
Homeschool Reports is live at homeschoolreports.com.au, serving Queensland families, and I have since added a second service for Home Education letters.
Technical breakdown
Homeschool Reports is a Next.js and TypeScript app on Vercel, pinned to the Sydney region. A parent fills a multi step questionnaire that saves as they go, pays by speed tier through Stripe, and the paid webhook sets the due date and sends the emails. I work the orders in an admin dashboard and download a bundle to write the report by hand. The whole app is built so that orders, files, email, and payments each pick the cloud service or a local fallback by a single setting, so it runs the same on my machine with nothing to configure.
The read-modify-write race that was losing 96% of writes
Appending an activity note to a lead read the whole record, changed it in memory, and wrote it back, so when two updates landed together one clobbered the other, and under load roughly 96 percent of concurrent writes were lost. I replaced it with atomic patches at the database, so an append is a single operation the database serialises rather than a round trip through the app. Any read, change, write on shared data is a race waiting to happen, so do the change in one atomic operation.
An admin login that fails closed
The admin auth had a fallback to a default password of changeme, which is a fail open, the worst kind of default. I reworked it to refuse by default, store the password only as a scrypt hash and never in plaintext, and use a signed expiring session token, so changing the password invalidates every existing session. Auth should fail closed, and a default credential is not a convenience, it is a backdoor.
Stopping questionnaire tokens leaking to the ad pixel
The marketing pixel was receiving questionnaire tokens in its events, quietly sending identifiers to a third party, and the base pixel code was also initialising twice. I stopped passing any token to the pixel and load the base code once. Analytics and ad pixels forward whatever you hand them, so treat what you send an ad network as carefully as what you store.
Deletion that actually deletes, plus retention
Some products capture a child's health and disability details, so deletion has to be real, not a hidden flag. I built true deletion, defined a retention period, and added a weekly cron that sweeps old data on its own. If you hold sensitive data, deletion and retention are features you build and schedule, not a promise in a policy document.
One source of truth for price and turnaround
The prices and turnaround times shown on the marketing page had drifted from the values used at checkout and in the due date clock, because they were written in more than one place. I made a single tiers file the one source, and the page, the Stripe checkout, and the clock all read from it. Any number that appears in two files will eventually disagree, so give it one home and import it everywhere.
A due date clock anchored to work, and paused overnight
The twelve hour emergency tier should not burn its clock while everyone is asleep, and it should not start the moment the customer pays but before they have finished the questionnaire. So the due date is anchored to questionnaire completion, not payment, and it pauses outside business hours in Brisbane time. A promise about time has to model the real world it is measured against, the working hours and the actual start event, not a naive countdown from checkout.
Dual backend, zero config, one function signature
Orders, files, email, and payments each choose a cloud service or a local fallback by a single environment variable, Postgres or JSON files, Vercel Blob or disk, Gmail or the console, Stripe or a mock, and every one keeps the same function signature either way. So the whole app runs on my machine with nothing to configure, and production only swaps the implementations. Coding to one interface with a local and a cloud implementation makes an app both easy to run and easy to reason about.
Making the database the record, and email only the notice
Outbound email goes over Gmail, and customer replies come back through an inbound webhook, verified with a signature and stored in the database, which is the real record of the conversation, with the email only a notification. A daily cron emails me a digest. When you build on email, decide early that your database is the truth and the inbox is just a copy, or the two will disagree.
Links