Migrating a founding subsystem to Engaging OS

The circle is completed by powering this teacher certification system with the latest incarnation of the thinking that began with it.

A membership body for independent schools runs Engaging OS for event registration, but until now alongside it ran a legacy system to power teacher certification.

Set up by Engaging almost a dozen years ago, the certification system had remained in daily use and required minimal maintenance.

With the advent however of modern Engaging OS running a sibling sub-system, the question arose: to merge or not to merge?

The rationale

There was in truth little doubt we would migrate and merge. The role-based setup that ran the teacher certification system is after all the precursor to Engaging OS, so how fitting to bring it up to speed and give it the latest incarnation of the thinking initially devised during its development.

For users, because the interfaces for the event registration and teacher certification sub-systems are now unified, there is less cognitive load; once you’ve learned how one works, you’ve learned the other as well. Also, for administrators at member schools, it would be frustrating to need to maintain their list of staffers twice. Indeed, since the two systems are quite similar in features writ large, and serve the same 10,000 or so users, it would be almost comically redundant to have two parallel systems doing similar things. 

Maintenance and development is also more efficient when working on one platform rather than two, especially when the second is relatively cumbersome to work with and increasingly outdated. In the intervening years since the legacy ExpressionEngine system was built, web development has made significant advances. There is now compilation, so that the developer can work in an environment best suited for development, whenceupon the code is then converted into something best suited for usage. There is now componentization, the shape best suited for developers now possible thanks to compilation. And there is reactivity, which updates interface objects automatically based on changes made elsewhere. Together these coalesce into a new generation for the craft. And among the best expressions of this new generation is Nuxt, Engaging OS’s frontend framework.

So despite the system’s stability, having a modern system in such proximity—same developer, much of the same data and functionality—made migrating and merging too easy a win to pass up.

The mechanics

The first step in migration was to map ExpressionEngine field ids to the target field in Directus, eg, field_id_77 is Start_Date. Then an SQL query for each collection produced a CSV file for each collection. The CSV file headers were the target system’s field names, among which were integer fields to store all the legacy entry IDs.

Each CSV was imported into the new system with a dedicated import script, then another integration script was used to forge the relationship between the various tables. These used the imported legacy IDs to populate relationship fields or generate relationship tables. A file script worked in batches to import files, skipping any already copied, as scripts were inevitably run multiple times to make corrections and clean data.

AI helped this time, producing the queries and scripts quickly. It added niceties such as summaries, progress reports and validation checks that we likely would have skipped had we been hand-coding.

Prompted, Claude also produced a checklist, even dividing items into sections, which we printed onto paper and worked through with a pen, full-on Checklist Manifesto-style. The paper checklist definitely helped calm nerves. 

Rationalized data architecture

For its backend, Engaging OS relies on Directus, one reason for the choice being that it keeps the database intact. A collection in Directus is directly a table in the database, a relationship handled the way a database relationship should be handled. Metadata lives in separate Directus system tables; if Directus were to disappear tomorrow, the system’s data would still make total sense.

In contrast, the legacy ExpressionEngine system took a different approach, squeezing all content types into a single shared table. This fundamental architectural trade-off makes many some things possible but eventually kills efficiency as you end up battling the database rather than ebbed along by it.

So this migration wasn’t just moving data sideways but was a data architecture improvement. Fields in the database are now labeled exactly as they appear on the frontend. There need be no more migrations of the actual data.

Extended functionality

Engaging OS is still sufficiently new that each new instance requires at least some new features. In this case we needed to add two. The first is field layouts within ports. The Inservice Responses port is for reporting the count of each participant’s answers to questionnaires regarding the quality of the activity in which they just participated. We had laid this out as a grid, but ports in Engaging OS display each field as a row. So we created a new Field_Layouts JSON field and added the first one: grid.

The second new feature is more profound. In Engaging OS, ports are edit interfaces—open one to work with a single record—while consoles are list interfaces—they show multiple records and provide conduit links through to their ports. Ports display each field as a row, which works well for most things, but not the Inservice Participations port, which show participations within an activity. That’s not a field; it’s a related collection, with many entries.

The solution, once we saw it, was almost obvious. Consoles already display lists of records and already link to ports. So embed a console inside a port! And when opening a port from an embedded console, don’t give it a full screen, but slide it open like a drawer! This is more or less how Directus does it when handling multiple relationship fields.

With this single addition, ports can now display related lists using an existing primitive. It turned out there was no need for a new list-rendering component or special table widget; the maximally powerful option already existed.

We needed one further refinement: making this a checkbox-powered roster. So if an embedded console has both a “status on” and “status off” value defined, it renders checkboxes instead of its normal layout. Each line becomes a toggle; the user checks a box to add a participant (in the background creating a new entry if one doesn't exist for that user), and unchecks to remove them.

A wider payoff

It turns out this matters way beyond teacher certification. The event registration system has a growing problem: the Event Registration port displays one row per registrant, but as we add more options per event attendee, this row grows increasingly wider and unwieldy.

As an embedded console, the rows of registrants can become more streamlined, linking instead each to its own drawer port, where all aspects of this attendee’s registration can be managed. The architectural pattern born from the certification migration improves the heart of event registration.

Conclusion

Although it’s a relief to no longer need to deal with the legacy inservice engine, the migration should be considered not a negation but rather a confirmation of the initial architecture, flowering as it did over the years into a much more powerful, flexible platform. This migration feels like full circle, with the venerable precursor gaining zippy new life.