Case study · Client platform

AMX Counselling Centre

The system a career-guidance centre in Larnaca now runs its day on. One web app for the calendar, the client register, receipts and statistics — for the counselling centre and the tutoring school it also runs — with five years of meetings and documents moved in from the old CRM and the office's folders.

Private platform — no public URL. It holds the records of students and their families, so there is nothing to link to.

The AMX week calendar on a laptop: appointments for Thursday to Saturday, colour-coded by status, with the month and today's list in the sidebar
The week view the front desk works from. Every screenshot on this page is from the demo environment, seeded with fictional clients — the live system holds the records of real students and is not shown.
1,400+ Client records
2,968 Past meetings migrated
5,005 Documents moved in
66 End-to-end tests

The problem

The centre's work was spread across three places that did not know about each other: an old CRM for meetings and contacts, a separate system for receipts, and years of documents in folders on an office computer.

A family calls to book a second meeting. To answer well, the person at the desk needs the student's history, which class they are in, what was sent to them last time and whether the last session was paid — and that meant three windows and a good memory. The tutoring school added a second register of students on top, with brothers and sisters often sharing one record.

The centre did not want a bigger CRM. It wanted one simple screen per job, that anyone in the office could use on the first day.

The approach

I built it from scratch as a small, framework-free web app: vanilla JavaScript ES modules on Firebase, with the data kept in the EU — Firestore in a European region and the documents in an EU storage bucket.

There is no server of my own and no Cloud Functions. Firestore Security Rules are the back end: every write is checked there — the shape of a client record, who may touch the statistics PIN, whether a receipt number follows the last one. That keeps the system cheap to run and leaves nothing on a server to patch, which matters for a small office that will never have an IT department.

What I built

Four areas, each one screen deep, with an illustrated guide inside the app.

  • Calendar — month, week and day views on desktop and an agenda view on the phone, built around the centre's own opening hours. Drag to move, overlaps refused, pending and confirmed bookings, a waiting list, undo with Ctrl+Z, keyboard shortcuts, and bookings that save offline and sync when the connection returns.
  • Client register — one register for the counselling clients and the tutoring students, each tagged with the unit they belong to. School classes move up by themselves every 1 September. Every client has a history, notes and their documents.
  • Groups by service — who had which service in a period, so the right information leaflet goes to the right families, with the emails ready to copy and a tick for who already received it.
  • Receipts — issued from an appointment or by hand, with discounts and paid/unpaid, printed as an A4 PDF. The numbering carries on from the centre's previous system, so the books have no break.
  • Statistics — appointments, cancellations, no-shows, income and new clients by month and year, per service, per unit and per class, behind a PIN.
  • Backups — full JSON and CSV exports, and a restore that only brings back what is missing.

Technical decisions

Four choices shaped the system, and each one cost something.

Security Rules as the back end, including the receipt counter

A receipt number has to be unique and the sequence must not skip. Without a server, the rules enforce it: a receipt and the counter are written together in one transaction, and the rules refuse any receipt whose number is not exactly the next one, any change to an issued receipt other than marking it paid or cancelled, and any deletion. A cancelled receipt keeps its number.

Cost: logic in Security Rules is harder to read and to test than ordinary code, and some of it has to be mirrored in the front end for friendly error messages. Every rule is exercised by end-to-end tests against the Firebase Emulator, because a rule that is only read is a rule that is assumed.

A PIN on the statistics, set by a link and never stored

The centre wanted the numbers out of sight of the front desk. The statistics page asks for a PIN; there are two, the owner's and one for support, each set through a one-time link and stored only as a salted PBKDF2 hash. Nobody — including me — can read a PIN back, and after five wrong tries the page makes you wait.

Cost: it is a lock on a screen, not on the data. The same staff accounts can see appointments elsewhere in the app, so the PIN keeps the totals out of view, not out of reach — and I said so when we agreed on it. A forgotten PIN cannot be recovered, only replaced with a new link.

Imported history that counts but does not earn

Five years of meetings came in from the old CRM. They belong in each client's history and in the appointment counts, but not in the income: what was charged back then is not known, and today's prices would invent revenue that never happened. So imported meetings carry no price, count in the statistics without adding to income, and can never be put on a receipt.

Cost: two kinds of appointment now live in one collection, and every place that reads a price has to know the difference. The income card says how many old meetings it is leaving out, rather than show a quiet zero.

Documents uploaded straight from the office computer

The documents — 1.5 GB of applications, transcripts and certificates — went from the office's folders directly into the EU bucket, each one through its own short-lived signed upload link. The service-account key never left my side; the computer only ever held links that can write one file each, for one day. Every file was checked for size and type after upload and only then attached to its client.

Cost: a signed link is a write permission for as long as it lives, so the list of links had to be treated as a secret and deleted as soon as the upload finished.

What was hard

The migration, by a distance. The CRM, the tutoring school's student list and the document folders had no shared ID — the only thing linking them was a person's name, typed by different people over five years. Names appeared in either order, with Latin letters mixed into Greek words, with misspellings in the meeting titles (“ΣΥΝΑΤΗΣΗ”) and with numbering that sometimes restarted every school year and sometimes never did.

So nothing was guessed. Every match had to be unique to be imported; namesakes and people missing from the contact export were held back and listed for the centre to decide, instead of being attached to the wrong family. Siblings who shared one record in the tutoring list were split into separate students, with their class worked out from the year of birth. The client and meeting imports were rehearsed on a copy of the real data in the Firebase Emulator first, everything ran after a full backup, and every step was safe to run twice.

The second hard part was the opposite of technical: an office that has used the same CRM for years has habits. The answer was to keep each screen to one job, write the guide inside the app with real screenshots, and let the numbers live only in the statistics, so the screens people use all day stay calm.

The result

The centre runs its calendar, clients, receipts and statistics on the platform, with 1,400+ client records, 2,968 past meetings back to 2021 and 5,005 documents already in place on day one.

About 9,800 lines of JavaScript and CSS, no framework and no server, covered by 66 end-to-end tests. What I take from it is that the code was the smaller half of the job. The larger half was moving five years of someone else's records across without losing or misplacing any of them — and being honest, in writing, about the ones that could not be placed.

Have a project in mind?

I take on client websites, web apps, and product builds across Cyprus and Greece.

Start a conversation