Baya Education
A production learning platform for an Egyptian Grade-12 chemistry course — sold by the module, with lecture content that genuinely cannot be downloaded, quizzes graded on the server, and a staff portal that runs the business end of it. It has real students on it today.
Sell a module, grant access for a year, take it back when the year is up, protect the paid material properly, and give a non-technical person one screen that answers who paid, who has access, and what happened.

- Role
- Design & build, end to end
- Scope
- Platform, course and staff portal — 115 routes
- Stack
- Django 5.2 LTS · PostgreSQL · Railway
- Supplied
- Four source booklets, a brand board, module artwork
The brief
The client is a chemistry teacher with a Grade-12 course and four source booklets. What existed was an interactive single-page course for the first booklet — no accounts, no payment, no way to stop anyone saving the page and passing it around, and no way to answer a student who says they paid and got nothing.
The ask was a business, not a website: sell a module, grant access for a year, take it back when the year is up, protect the paid material properly, and give a non-technical person one screen that answers who paid, who has access, and what happened.
Two constraints shaped the architecture. Payment in Egypt is manual — bank transfer and Vodafone Cash, verified by a person against a statement — so access could never hang off a payment webhook. And the existing course was eight thousand lines of working vanilla JavaScript with no build step, which is an asset rather than debt. Django was built around it, not through it, and the course frontend needed zero changes.
What shipped
- Platform home, module catalogue, account and full authentication — login, reset, forced first-login password change
- Four roles, with module access granted for a year and enforced on the timestamp
- Paid lecture bodies served only through an access check, never as static files
- Quizzes graded entirely on the server, drawing a fresh paper per attempt from a question bank
- Per-lecture quiz settings — length, difficulty mix, pass mark — frozen onto each attempt as it starts
- Progress synced across devices, with passing derived from graded attempts and never accepted from the browser
- A staff portal of 18 screens: enrolment queue, students, grants, question bank, email log, audit trail
- 20 transactional emails, provider-independent, with delivery tracked as its own state
- An HMAC-signed webhook bound so a form can only ever request its own module
- 12 simulation engines that solve their physics rather than animate a stored answer
- Deployed with a real domain, HSTS, a content security policy, rate limiting and a one-device-per-student lock
- routes
- 115
- tests
- 446
- simulation engines
- 12
- lines, design and backend
- 30,100

Decisions, and why
Anyone can list a stack. The useful part of a case study is the calls that had a real alternative — so each of these says what the tradeoff was, not just what was chosen.
- 01
The paid lectures are not files the server can reach
Every lecture body lives outside the static tree, and a test fails the build if one appears inside it.
A file that is served can be fetched, guessed, linked or scraped, whatever is checked before it is rendered. So the bodies were moved out of the web root entirely and are assembled per request, after the access check has already passed. The whole of the content protection is that one structural fact rather than a rule somebody has to remember.
Tradeoff · The content is rendered by the application rather than a CDN — a cost of nothing measurable at this scale, in exchange for a paid course that is not one right-click away from being free.
- 02
Expiry is enforced by the clock, not by a label
Every access check filters on the expiry timestamp. The nightly job only relabels.
The obvious design is a scheduled job that flips grants to expired and a check that reads the status. It fails silently in the customer's favour: if the job stops running nobody reports it, and everyone keeps access they stopped paying for. Reversing it means access lapses exactly on time whether the job ran last night, last month, or never.
Tradeoff · A stale label becomes a reporting detail rather than a way in, and the dashboard states that in words so nobody has to infer it from a number.
- 03
The browser used to hold the answer key to its own exam
One function turns a question into JSON, and it assembles the correct answer inside the branch that runs only after the student has answered.
The original course shipped with every option carrying its own correctness flag in the page source. The rebuild moved grading to the server and made the leak structurally impossible rather than merely absent: an unanswered question has no code path that carries the answer, the explanation, or an ordering that hints at it. Answering the same question twice is refused, because otherwise a client can submit every option in turn and keep the one that came back correct.
Tradeoff · A network round trip per answer instead of an instant local check — and a test that reads every lecture body and fails if the old markup ever returns.
- 04
The enrolment form files a request. It cannot grant access.
A form submission creates a row in a queue. A person approves it against a bank statement.
Payment is manual, so any path that turned a submission into access would hand the paid course to whoever found the form URL. Stated as a consequence: if the integration secret leaked tomorrow, the worst available outcome is rows in a queue that a human rejects.
Tradeoff · The system carries a second, independent payment-review state, so the money question stays answerable after the access question has been settled — and the client can move the gate whenever their operation is ready for it.
- 05
A failed email can never undo a successful enrolment
Sends happen outside the transaction, and the send function never raises into its caller.
The worst outcome available in this product is a student who paid, whose access was granted, and who is told nothing because a mail server was briefly unreachable. Delivery is therefore tracked as its own state with its own retry, beside the enrolment rather than inside it.
Tradeoff · The exception handler is deliberately broad. Every provider raises something different, and none of them may be allowed to reach an approval.
- 06
The molecule viewer solves the physics; it does not draw the answer
Every electron domain repels every other on a unit sphere, and the shape you see is the equilibrium.
Hard-coding the geometries would have been a fraction of the work and identical in a screenshot. It would also have been a picture of the answer: bond angles compress from 109.5° to 106.3° to 103.2° across methane, ammonia and water because lone pairs carry more weight in the same equation, not because three numbers were typed in. A student can drag the molecule out of shape and watch it find its way back, which is the entire pedagogical point.
Tradeoff · A simple inverse-square model settles within about 1.3° of the textbook figures, so the ideal angle is shown beside the computed one — which turns the gap itself into something a student can ask about.
- 07
One shell, one registry, one palette per module
Adding a module is a data file and a colour file, both selected by number.
The course shell is subject-agnostic. Everything that makes a module its own world — its lectures, routes, engines, search index and accent colour — lives in two files chosen by the module's number, so the sidebar and the dashboard build themselves and the shared shell is never edited to add content. The four booklets the client already has are four data files, not four rebuilds.
Tradeoff · The shell was written when there was one module, so its colour variables still carry that module's name. The second module proved the mechanism — 63 hard-coded colour literals became one themeable token — and the renaming is tracked.
The simulation contract
The course has twelve interactive engines and one rule they all obey: an engine computes its result rather than drawing it. That is not a stylistic preference. A result that was typed in cannot be pushed on to see whether it holds, and a student cannot discover anything from a picture of an answer.
Concretely: the gas laboratory contains no per-law formula anywhere in the file — Boyle, Charles, Gay-Lussac and Avogadro all emerge from holding a different variable in the same equation of state. The titration curve solves the charge balance including the autoionisation of water, which is why the pH lands at exactly seven at equivalence instead of running to infinity and needing a clamp. The heating curve's plateaus are not drawn flat; a phase transition absorbs its entire latent heat before the next degree is possible, so the flatness is a result rather than a shape.
The heating curve was built three times, and the sequence is the most useful thing in this section. First as apparatus beside a chart: legible on sight, cheap to draw. Then as sixty-four particles in a rotating three-dimensional box — far more impressive, and much worse, because it assumed the student already knew that speed and freedom are different quantities, which is the exact thing the lecture exists to teach. What shipped is the first version drawn properly, with the teaching moved into a ladder of five bars, each as long as the energy its stage costs. When an engine feels cheap, check whether it is drawing a picture of the answer. When it feels clever, check whether it assumes the thing it is meant to teach.
Security, tested as an attacker
A review was run as an attacker rather than as a checklist: 39 probes across eight identities — anonymous, an enrolled student, a lapsed one, a revoked one, a suspended one, a second student, the course owner and the administrator. Direct object references on lectures, attempts, progress, bookmarks, email logs and enrolment requests; walking sequential ids; path traversal on the content slug; a tampered content path written straight into the database; client-reported scores and pass marks; privilege escalation; cross-site request forgery on every state-changing route.
All 39 were refused by the application as it stood, and the probes stay in the test suite so that cannot quietly stop being true.
What came out of it: a content security policy with no inline script and no eval, which required removing 56 inline event handlers; rate limiting on the authentication paths; and a one-device-per-student lock. That last one had two obvious implementations and both were rejected — an IP address because a phone changes it hourly and a school shares one, a device fingerprint because every input to it is supplied by the person it is meant to identify.
Two identities in one product
The application is a platform that contains modules, not one module with more added later, and the visual separation is enforced by loading rather than by naming. A platform page never loads the course's stylesheet; a course page never loads the platform's. The result is that the entire course frontend — the part that already worked — needed zero changes when the platform was built around it.
The platform shell is cool, editorial and deliberately quiet: the colour on it belongs to the modules, each of which supplies its own accent to its own card. Entering a module gives its full-screen world with none of the platform's chrome, and the only platform element inside it is one link back out, styled in the module's own tokens.
The whole thing was measured at three widths in both themes: no horizontal overflow at any width, contrast passing AA everywhere, and with motion disabled none of the twelve revealed elements sits at less than full opacity — which is the state both a reduced-motion visitor and a visitor with JavaScript off receive.
What was deliberately left out
Scope discipline is part of the work. Each of these was considered and cut for a stated reason.
- Certificates and ranking
- Scores are practice data with unlimited retries. A certificate would make that dishonest.
- A question importer
- It was built, and it loaded 329 draft questions from the source booklets. A parser over a document is guessing, and a mis-keyed answer marks a student wrong on a question they got right. Removed, with its dependency.
- An in-browser AI assistant
- It existed in the original build, calling a model API from the browser with no key and no proxy, so it could never have worked. Removed at the client's request.
- A task queue
- Nothing here needs a broker. Email sends inside the request; background work is two scheduled commands.
- Database row-level security
- Two authorization systems that have to agree is a bug factory. There is one access decision, in one function.
The platform is in service — students are enrolled on it and using it — but the client has not opened their public launch yet, so it is being run quietly by design rather than sitting unfinished. Figures on this page were measured against the repository on 2 September 2026.
Stack
- Django 5.2 LTS
- Python
- PostgreSQL
- JavaScript
- Railway
Services
- Product design
- Backend engineering
- Frontend engineering
- Interactive simulation
- Security review
Need something built to this standard?
We design and build complete systems — a purchase path that works, roles, and an admin panel that actually runs the business. And Arabic here is never a translation layer bolted on at the end.
