V.Trivedy_
← Architectural Teardowns

Pedagog: digital arm of European International University (EIU), Paris providing gamified learning and blockchain certification

A white-label learning platform. Any institution can onboard itself, non-technical teachers can author any course format, paid live classes run on self-hosted BigBlueButton, and completion certificates are minted as on-chain NFTs. The hard part was none of those. It was a single content model general enough to represent anything and simple enough for a teacher who has never opened a CMS.

Snapshot

RoleLead engineer and architect, on a team of three: the author, one front-end dev, one designer.
DomainMulti-tenant ed-tech / LMS, built for an international university
StageClient build. Shipped Sept 2019, still in production at pedagog.ac (later modified by others).
Core stackLaravel (PHP), MySQL, Blade + Bootstrap, vanilla JS/jQuery, Redis (queues + cache)
Live videoSelf-hosted BigBlueButton (WebRTC) via the BBB HTTP API
BlockchainCertificate NFTs minted through a third-party minting service
IntegrationsGoogle Calendar (OAuth), email/SMTP, payment gateway
StatusShipped Sept 2019, still in production at pedagog.ac (later modified by others).

The problem

One platform that many universities can onboard themselves onto. Each manages its own teachers, staff, content, courses, paid live classes, and revenue. They share one marketplace and one gamified student experience. Five forces make that more than a CRUD app.

RequirementWhy the obvious build breaks
Teachers author any format: text, notes, file, audio, video, transcript, quizzes, full assignmentsModel each course type as its own table and flow, and every new format becomes a migration plus a new UI plus new code. The work grows combinatorially. The schema ossifies around last year's catalogue.
The authors are not technicalA schema flexible enough to represent anything tends to surface as a UI complex enough to defeat the person using it.
Live classes at real concurrencyReal-time WebRTC is CPU- and bandwidth-bound. A single media server tops out near ~200 users, and a live meeting can't be split across servers.
Certificates must be verifiable beyond the issuerA row in your database proves nothing once the issuer is gone. Verification has to outlive the platform.
External systems fail: calendar, email, chain, paymentsPut any of them inside the request/response cycle and a third party's bad afternoon becomes your user's error page.

The instinct is to model the catalogue you can see: "video course," "quiz course," "PDF course." That instinct is the trap. The catalogue you can see is a snapshot. The one you'll be asked for in six months isn't in it. So you model the grammar of a course, not its current vocabulary. And you accept up front that the schema will be able to express things the authoring UI has no business showing all at once.


The architecture

Everything hangs from one decision. A single Laravel core owns identity, content, assessment, commerce, gamification, and reporting, all against one MySQL database. Everything slow or failure-prone gets pushed off the request path into Redis-backed queue workers: video provisioning, blockchain minting, calendar sync, reminder email. After that, the system splits into planes along one line: what fails and what's slow. A synchronous app plane that has to stay fast and correct. An asynchronous worker plane, and a bandwidth-heavy media plane, both allowed to be slow, to retry, and to fall over now and then without taking a user's click down with them.

In practice they were decoupled workers around a Laravel monolith. That is the point. The boundary worth drawing was the failure domain, not the deployment unit. Split for fashion and you buy distributed-systems pain with none of the payoff. Split along the things that stall and break, and you buy a site that stays up when the chain doesn't.

How a request fans out across four planes (the fast app core, the retry-friendly worker plane, the bandwidth-heavy video plane, and third-party services) so a congested chain or a saturated media node never lands on a user's request.

Key choices and what each one cost

DecisionChosenOverBecause
Course/content modellingPolymorphic, composable content blocks in a course → module → lesson → block treeA table per course type, or one untyped JSON blobA typed block registry represents any format with no schema churn, and it keeps each block validated. Raw-JSON "flexibility" rots into data you can neither query nor trust.
TenancyShared MySQL with institution_id scoping enforced by a default query scopeDatabase-per-tenantA cross-institution marketplace and one platform-wide leaderboard need cheap cross-tenant reads. DB-per-tenant makes those queries and migrations painful.
External workIdempotent jobs on Redis queuesSynchronous calls in the controllerEmail, calendar, payments, and minting all stall and fail. The request path must not inherit their latency or their outages.
Live videoSelf-hosted BigBlueButtonA SaaS API (Zoom/Twilio)Self-hosting kept recordings, data residency, and per-institution cost in hand, and dodged per-minute pricing. The price was owning capacity and ops.
Video load balancingPlace each meeting on the least-loaded server at creation timeRound-robin every API requestA running meeting lives in one server's memory and can't be spread across nodes. So you balance whole meetings, not requests.
Certificate trustOff-chain credential plus an on-chain NFT minted by a third partySelf-run wallet/node, or DB-only certificatesA custodial minting API removes private-key and gas management. The chain provides tamper-evident, issuer-independent proof.
Front endServer-rendered Blade + Bootstrap + jQueryA heavy SPA frameworkA small team, content-dense and SEO-able pages, and broad institutional browser support favoured progressive enhancement over an SPA's build and runtime weight.

Data model

The schema is the centerpiece, not a footnote. The whole "represent any course" promise is kept or broken right here.

The spine of the system. A course is a tree. A block is polymorphic and type-validated. XP is a ledger of events, not a mutable counter. A certificate and its NFT are separate rows, so a mint can fail and retry without ever leaving the credential half-issued.

Five decisions keep the schema honest.

  1. Polymorphic content blocks. A content_block carries a block_type and a payload validated against that type. A new format (say, an interactive transcript) is a new entry in the type registry and a new renderer. Not a migration, and not a new authoring flow. One discipline stops this from rotting: validation lives at the block-type boundary, so payload is never a junk drawer. Skip the registry, let payload go free-form, and a year later you can neither query nor trust a single row.
  2. Submissions are stateful threads, not overwrites. The teacher↔student back-and-forth is a submission with a small state machine (draft → submitted → returned → graded) and an ordered submission_message thread. Returning work for revision is a state transition with history, not a destructive edit. Grades attach to the submission, so the trail survives.
  3. XP is an append-only ledger. activity_event records every XP-earning action and its delta. Level and totals are derived. The tempting shortcut is a mutable xp integer on the user, and it's the one that makes gamification impossible to audit, replay, or correct when a rule changes mid-flight. Same reason you never store an account balance as a lone number.
  4. Certificate and NFT are decoupled. The credential exists the instant the course is completed. The nft row tracks the on-chain mint as its own object with its own status. Minting can stall on gas or chain congestion and get retried on its own, and the certificate is never caught in a half-minted state.
  5. Tenant scoping is the default, not an afterthought. Every tenant-owned row carries institution_id, enforced by a global query scope. The failure mode here is universal in multi-tenant systems: one query that forgets the scope leaks one institution's data into another's dashboard. The defence is to make scoping the default, and opting out the explicit, reviewed exception.

Infrastructure & operations

The hardest data flow ties payment, calendar, video, and chain together. Every external hop in it is either webhook-driven or queued. Nothing slow or flaky sits inside a click.

The path a paying student takes from enrolment to a minted certificate. Payment is confirmed by a webhook from the gateway. Meeting provisioning is idempotent on the class id, so a retry can't spawn duplicate rooms. Minting is queued, so a slow chain delays a badge, not a checkout.

Where load actually bites, and how the design absorbs it

Pressure pointWhat goes wrong under loadHow the design absorbs it
Live-class concurrencyA single BBB server saturates near ~200 users. The HTML5 client's media path is CPU- and bandwidth-bound, and a meeting can't span servers.Place whole meetings on the least-loaded node. Scale out by adding servers. Cap webcams (the real cost), not raw headcount.
Co-locating app and mediaMedia bursts starve PHP-FPM of CPU. One busy class degrades the whole site.Keep the app plane and the media plane on separate hosts. The web app and a media server never share a box.
Spiky external calls (mint, mail, calendar, pay)A slow third party becomes user-facing 5xx and timeouts.Decouple into Redis queues with idempotent, retried jobs. The controller returns immediately.
Reminder fan-outThousands of due-date and class reminders fire at onceThe Laravel scheduler enqueues. Workers drain at a controlled rate instead of blocking web traffic.
RecordingsDisks fill, recordings scatter across nodesHarvest processed recordings off the media nodes into central storage.

The capacity numbers above came from load testing, not from a spec sheet. That's the only thing that ever tells the truth about a WebRTC server. The brief's account matches: iterative testing and tuning brought live-class load under control.

Security & isolation

  • Tenancy: enforced institution_id scoping isolates each institution's users, content, and revenue.
  • RBAC: institution-admin, teacher, staff, and student roles gate content authoring, grading, live-class creation, and access to earnings reports.
  • BBB join URLs are per-user and signed. The signature is an SHA checksum over the query string plus a server-side shared secret. The secret never reaches the browser, and whether a user joins as moderator or viewer is decided server-side, so a student can't promote themselves to moderator.
  • Payments activate enrolment only on a verified gateway webhook, not on the client's success redirect.
  • Minting is custodial through the third-party service, so no blockchain private keys ever live in the application.
  • Hosting and CI/CD: Digital Ocean, Kubernetes

Outcome

Strictly from the brief:

  • Shipped September 2019, and still in production at pedagog.ac after the engagement (later modified by others). The design kept running after the original team left.
  • Multi-institution onboarding, with each institution managing its own teachers, staff, and content.
  • A content model that in practice supports almost any course format. The flexibility target was met without per-format rebuilds.
  • A graded teacher↔student assignment workflow, with threaded discussion before scoring.
  • Paid live classes on self-hosted BigBlueButton, with Google Calendar sync and recurring reminder emails to students and other concerned parties.
  • On-completion certificates issued and minted as NFTs.
  • A gamified student dashboard: XP, levels, badges, and an avatar whose look and surrounding "universe" evolve with progress. Plus institution and teacher reporting on content and earnings.
  • Live-class load brought under control through testing and optimisation.

What I'd watch on a system like this

  • The polymorphic-blob trap. A typed block model is the right call, but it dies the moment payload becomes free-form JSON with no validator. Guard the type boundary, or lose the ability to query and trust your own content.
  • Here the Microservices split paid off because it followed the failure domains (video, chain, email), not because services are fashionable. Decompose along what stalls and breaks.
  • An NFT is only as durable as what it points at. If the certificate hash and metadata live only in your database, the on-chain token is a receipt, not independent proof. Anchor the hash (and ideally the metadata) off your own infrastructure, so verification survives the issuer. Otherwise the "blockchain" part is mostly decorative.
  • Balance meetings, not requests. Cap cameras, not people. The two mistakes that sink live video at scale are round-robining API calls (a meeting is sticky to one node) and budgeting by headcount (webcams and screenshare, not listeners, are what melt a server).

The schema could represent anything. The teacher could not. Reconciling those two facts was the work: full expressive power underneath, progressively disclosed simplicity on top. And it lived as much in the authoring UI as in the database.