PROJECT 004 · CASE STUDY
East Melbourne Language Centre
A language school's marketing site, course catalogue, and enrolment checkout, built solo as a contract. A Google Sheet the staff already use is the entire back office.
Problem
East Melbourne Language Centre is a real school with in-person and online classes, corporate training, and VCE tutoring. Their old WordPress site could show a timetable but couldn’t sell a seat, and the staff are teachers, not CMS administrators.
The brief was a fast marketing site with a real enrolment flow: browse the timetable, take a placement test, pay for a class. The constraint that shaped everything was the client. Whatever ran the catalogue had to be something the office could edit without me.
What I built
The whole thing, solo, over about three and a half months.
- A static Astro site on Firebase Hosting, with Tailwind, structured data, and a curated set of 301 redirects to carry over the WordPress-era SEO.
- A Google Sheet as the CMS and back office. Timetable, pricing, course materials, and term dates are tabs in a spreadsheet the staff already knew how to edit. The build reads it to render the catalogue, and completed enrolments are written back to an Enrolments tab.
- Checkout on Stripe Elements with Python Cloud Functions behind it: PaymentIntent creation, webhook handling, and receipt plus admin emails through Resend, all pinned to the Sydney region with AUD as the only currency.
- An adaptive placement test: a CEFR level state machine from A1 to C1 that skips ahead after four straight correct answers, fails a level after five wrong, and recommends a course with the matching textbook. Progress survives in localStorage for 48 hours.
Decisions worth explaining
The spreadsheet is the database, and that’s the feature. No admin panel, no CMS login, no training sessions. The school changes a price or adds a class by editing a sheet, and the site picks it up. For a client this size, removing the software between them and their data was worth more than any dashboard I could have built.
Never trust the cart. Cart state lives in localStorage, which means a motivated user can edit it. So checkout re-prices everything server-side: the Cloud Function reads the live sheet, compares against what the client claims, and rejects the payment outright on any mismatch over one cent. The static site is the fast path and the function is the authority.
Idempotency without a database. Stripe webhooks can fire more than once. Before appending an enrolment row, the function searches the sheet for the payment intent ID and skips duplicates. A database would do this better, and a database would also be one more thing for nobody at the school to maintain.
Keep the fallback human. Not every language student wants to type a card number into a website. The checkout offers a “prefer bank transfer? call us” path inline, because losing those enrolments to purity would have been a bad trade.
Outcome
Live at languagecentre.net.au, taking real enrolments and real payments since March 2026. The school runs its own catalogue and I haven’t needed to touch the content once since launch.
Stack
Astro and TypeScript on Firebase Hosting, Python Cloud Functions, Stripe, Resend, Google Sheets as the data layer, deployed through Cloud Build.