Joshua DiniegaSoftware Engineer
Projects

Java Course Hub

The platform my ICT class runs on, with its own purpose-built MySQL engine and a demo that hands you your own classroom.

What it is

A classroom platform for the Java course I teach: lessons released one day at a time, practised through in-browser games that execute real SQL, with per-activity turn-ins the teacher reads to see who is stuck.

Why I built it

Convenience, plainly. I wanted the course I teach — lessons, practice, turn-ins — in one place I control instead of spread across four different tools.

Convenience, not necessity, and it's worth being exact about that: the version of this story with an obstacle in it would be a better story and wouldn't be true. My students' machines are fine. Nothing here was blocked without it. I teach this course and write most of it the night before I deliver it, and I wanted the whole thing in one place I control — lessons I can release a day at a time, practice that opens in a browser tab, turn-ins that land somewhere I can read between classes. The engineering underneath is real. The reason for it is just that it's easier this way.

In the app

Captured from the public demo, which seeds a throwaway cohort per visitor and deletes it within 24 hours. Every person in these frames — the student, the teacher, the classmates — is invented by the seeder.

By the numbers

lines in the SQL engine
1,024
tests: SQL engine + combat rules
59
game engines on one shell
8
lines of TypeScript
~18,500

Counted against the source on 12 August 2026, not estimated.

The engine, in one exchange

This is the shape of every lab in the course: the error is the lesson. The engine reproduces MySQL’s real error codes and wording — the exchange below is its verbatim behaviour, and clearing a “break it on purpose” task means producing exactly this error for real.

sql> SELECT * FROM students WHERE name = Ramon;

Error 1054: Unknown column ‘Ramon’ in ‘where clause’

sql> SELECT * FROM students WHERE name = ‘Ramon’;

1 row in set — task cleared ✓

Decisions that mattered

6 of them. Each opens onto what I did and the reasoning behind it.

  1. A purpose-built mini MySQL engine, judged by effect

    What I did, and why

    A 1,024-line parser and interpreter runs the course's SQL command set in the page — DDL, DML, SELECT with WHERE and ORDER BY, SQL_SAFE_UPDATES — reproducing MySQL's real error codes and wording. Tasks are marked by comparing resulting state against a canonical solution, not by matching query text.

    Marking on text means one blessed spelling of a correct answer, which teaches students to guess what the grader wants. Marking on effect means any correct query passes. It also makes 'break it on purpose' labs possible: you clear the exercise by producing the same real MySQL error, which you cannot fake without understanding it.

  2. The demo builds you a disposable classroom, then proves its own security

    What I did, and why

    Clicking the demo mints a whole cohort — a teacher, a student for you, three classmates with work already turned in — stamped with one cohort id and swept after 24 hours. Every teacher-side policy reads is_teacher() AND same_cohort(). The seeded submissions are written by those demo accounts themselves, not injected with the service-role key.

    A shared demo account is worthless for a classroom app: the first visitor's mess becomes the second visitor's first impression, and you cannot show the teacher side without showing someone real. Per-visitor cohorts fix both. Seeding as the users is the part I would defend hardest — rows written with a service-role key prove nothing, but rows that had to satisfy the same policies as a real turn-in mean every demo launch re-runs a smoke test of the authorization rules. If the policies break, the demo breaks before a student does. The entrance is an unlisted route, not a button on the login page, because a demo cohort has every lesson released and my own students would have used it to read ahead.

  3. Signup is gated by a database trigger, not a redirect

    What I did, and why

    Enrolment is invite-only against a teacher-managed class list, enforced by a Postgres trigger that aborts any signup for an unlisted email at the transaction level. Supabase's own 'allow new signups' setting stays on.

    It makes the public /register page safe by construction: there is no route, guard, or middleware ordering that can be wrong, because the check is not in the application. Leaving signups open and letting the database refuse them is what lets allow-listed students register themselves without me minting accounts by hand.

  4. Content is data, not pages

    What I did, and why

    A week is a plain object, one file per day, rendered by a single template. Adding a week of material involves no JSX.

    I write this course while teaching it, usually the night before. Anything that makes publishing a lesson a code change is something that will eventually stop me publishing the lesson.

  5. Handing work back re-locks the day, against cached progress

    What I did, and why

    Days release from a single-row course_state table; steps inside a day unlock as work is turned in. When a teacher returns a submission, that step re-locks even if the student's browser has it cached as complete.

    Progress cached client-side is a performance decision that quietly becomes an authority decision. The server has to be able to take a step back, or 'returned for revision' is advisory and the student never sees it.

  6. One submission path, after eight copies of it silently drifted

    What I did, and why

    Every game used to carry its own copy of the same six lines: build a summary, POST it, set a saved flag, mark the step complete. When paste-tracking and a started-at timestamp were added to that payload, they reached only the three games open that day — no error, no type failure, just a signal the teacher believed was on everywhere and was on for three. The eight games now share one `useTurnIn` hook that builds the payload once, and a boss battle's combat rules moved the same pass into a pure `combat.ts` with no React or fetch in it.

    A duplicated literal doesn't announce the copies you didn't edit. The bug stayed invisible until a teacher noticed which games' submissions were missing a field, which is a slow way to find out a feature is two-thirds absent. Centralizing the payload means a future field is one edit. Pulling the combat rules into pure functions is what made them testable at all, since the win/loss logic had been spread across three `useState` setters where the only way to check it was to play the fight.

Stack

  • Next.js 16 (App Router)
  • React 19
  • TypeScript (strict)
  • Tailwind v4
  • Supabase (Postgres + RLS)
  • Vercel
  • Parsers/interpretersSQL tokeniser, executor, and MySQL-accurate errors
  • Postgres RLSsecurity boundary for all student data
  • Database triggersinvite-only enrolment enforced in-transaction
  • Next.js 16server components, teacher and student route groups

What is honest about it

  • Written fast, for a class that had already started. The engine is careful; parts of the surrounding UI are not.
  • The SQL engine and the boss battle's combat rules have test suites; the React components and the turn-in flow itself do not. Their behaviour is verified by a classroom using them, not by CI.
  • The demo cohort is disposable by design, so anything you build in it disappears within a day. That's the point, but it does mean you can't come back to your own work.

Want this kind of work on your codebase? Email me and tell me what you’re trying to build.