Case study · Client platform at Air Balloon Digital Studio
ABC Basketball Academy
The largest system I have built. An academy management platform that runs a real basketball academy of 200+ athletes end to end — athletes, groups, programmes, attendance, billing, payments and reporting. A studio project at Air Balloon Digital Studio, where I was the sole developer across all 30 sprints.
Private platform — no public URL. It holds the records of minors, so there is nothing to link to.
The problem
A basketball academy is not one business. It is a membership business, a scheduling business, a coaching operation and a small accounts department, and all four run on the same 200 children.
Every one of those jobs produces records the others need. Who is in which training group, and whether that group is already full. Which programme a family signed up to, and on which of four different fee arrangements. Who turned up on Tuesday. What a parent has paid this season, what is still outstanding, and whether a receipt was ever issued for it. Kept apart, those answers drift out of step within weeks; kept in spreadsheets, only one person in the building can ever answer them.
The second half of the problem is the parents. Every question they have — what do I owe, did my payment land, what is my child's pass — is a phone call to someone at the academy, and every unpaid fee is a conversation somebody has to remember to start.
The approach
I wrote the platform from scratch, framework-free: vanilla JavaScript ES modules on Firebase, no React, no build-time abstraction layer, nothing inherited from a template or a starter.
That is a deliberate choice at this size, not a shortcut. A system of ~35,000 lines with no framework only holds together if the structure is imposed by hand, so the architecture is 27 service modules that each own one thing — athletes, groups, programmes, billing, receipts, notifications — with the 21 admin screens composing them rather than reaching past them. What a framework would have given me in conventions, I had to give it in boundaries.
What I built
The platform covers the academy's whole operational cycle, across multiple seasons, in 21 admin screens.
- Athlete CRM — the athlete record everything else hangs off, kept across seasons rather than reset each year.
- Training groups — with capacity limits and waitlists, so a group that is full says so instead of quietly overfilling.
- Programmes and pricing — programme creation with flexible pricing models attached.
- Attendance, coaching notes and player progress — the coaching side of the record, on the same athlete.
- Billing — monthly, weekly, per-session and one-off fees, with partial payments and outstanding-balance tracking.
- Receipts — transactionally numbered, so the sequence has no gaps and no duplicates.
- Financial reporting — season-level reports with PDF and CSV export.
- Uniforms, sponsorships and expenses — the parts of an academy's books that are not fees.
- A parent portal — passwordless, installable as a PWA: player pass, payment history, outstanding fees, and online payment.
- Stripe Checkout — with webhook reconciliation into the academy's own ledger.
- Email and push notifications — including a scheduled monthly payment reminder.
Technical decisions
Four choices shaped the system, and each one cost something.
No framework, and a data model built around the season
Vanilla ES modules on Firebase, with the season as the axis the whole model turns on. An academy is not a list of members; it is the same athlete appearing across 2025–26 and 2026–27 in different groups, on different fees, with a payment history that has to stay attached to the year it belongs to. Getting that right first is what let attendance, progress, billing and reporting all be written against one shape.
Cost: no framework means no conventions handed to you. Rendering, state and module boundaries are all decisions someone has to keep making consistently — and with one developer, the only thing enforcing that consistency is discipline and the test plan.
A passwordless parent portal, installable as a PWA
Parents are the users you cannot train and cannot support. A portal that opens without a password, and installs to the home screen without an app store, removes both barriers at once: a parent checks what they owe and pays it without creating an account, and the academy never runs a password reset.
Cost: access with no password has to be scoped and revocable at the link level, because a link that needs no login is a link that can be forwarded. And an installable app caches: a service worker that serves a stale balance is worse than a slow one, so financial views have to be kept off the cache-first path.
Stripe as the payment rail, the academy's ledger as the record
Card payments go through Stripe Checkout, so no card data ever reaches the platform. But Stripe is not the academy's books — the internal ledger is, because it also holds cash payments, partial payments, uniforms and expenses. Webhook reconciliation is what keeps the two in step: Stripe reports the payment, the platform records it against the right fee.
Cost: webhooks are asynchronous and can arrive late, twice, or out of order, so every ledger write driven by one has to be idempotent. There is also a window in which a parent has paid and the academy's screen does not know it yet, and the UI has to be honest about that rather than pretend it is instant.
Receipt numbers issued in a transaction
A receipt number is not a display detail, it is an accounting artefact: it has to be unique, and the sequence has to have no gaps. That makes it the one deliberately serialised point in a database built for parallel writes — the number is allocated inside a transaction, so two receipts issued in the same second cannot collide.
Cost: a contention point by design. Every receipt passes through the same counter, and the write has to be retried rather than merely attempted.
What was hard
The billing model was the hardest part of the build. Four fee arrangements — monthly, weekly, per-session and one-off — multiplied by partial payments, mid-season joiners and balances carried across months, is not four cases; it is one balance calculation that has to be right in every combination of them. It is also the part where being wrong is not a bug report but a wrong number in someone's accounts, which is why the acceptance test plan grew to 131 tests.
The reminders were the second. A monthly payment reminder is trivial to send and easy to get wrong in a way that costs the academy its credibility: a parent who is fully paid up and receives a demand does not read the next message. So the scheduled job does not mail the parent list — it resolves who has a genuinely outstanding balance at the moment it runs, and contacts only them.
The third was that this is children's data, held by a client rather than by me. That is not a feature request, it is a constraint on the whole design, and it is the reason the security work below is not an appendix to this project.
Security and operations
The academy holds personal data on 200+ minors and the financial records of their families. Both were treated as system requirements, not as hardening added at the end.
- Role-based access for administrators and coaches, enforced in Firestore Security Rules rather than in the front end — a coach sees their groups regardless of what the client asks for.
- GDPR export and erasure workflows for minors' data, as operations the academy can actually run, not a policy document.
- An audit log, so who changed a fee or a record is answerable after the fact.
- Automated daily backups — unattended, because an academy has no IT department.
- A documented test plan of 131 acceptance tests, which is also what made the handover possible: the client received a system whose behaviour is written down.
The result
The platform is in production, managing 200+ athletes across the academy's full operational cycle, delivered as sole developer across 30 sprints from requirements analysis to client handover.
Roughly 35,000 lines written from scratch: 21 admin screens, 28 Cloud Functions and 27 service modules, framework-free on Firebase. What I take from it is not the size, though. It is that a system this wide, built by one person and then handed to someone else to run, only stays maintainable if the boundaries are real — service modules that own one thing, security enforced at the database and not the screen, and a written test plan that survives the developer leaving the room.
Have a project in mind?
I take on client websites, web apps, and product builds across Cyprus and Greece.
Start a conversation