Course Styles: every course, on brand
It started with a readability complaint: the course player’s gray-on-gray text was hard to read, and clients asked for black on gray instead. Fixing one color for everyone raised a better question: why not let each client set their own? The course player’s colors already lived in SCSS variables, so instead of reinventing the styling I exposed those same colors as settings: a default palette for each client, plus overrides for each course.
Every course is published as a self-contained package, so at publish time the chosen colors get injected back in as CSS custom properties, and the player’s styles read those variables where they used to read hard-coded Sass values. Same stylesheet, a different look for every client. All the defaults live in one place, so adding a new themeable color is a quick checklist and not an archaeology dig.
- SCORM
- xAPI
- CSS
- Sass
- Multi-Tenant Architecture
Bulk enrollment, rebuilt in eight phases
Enrolling thousands of learners at once (one client’s member group alone is 4,780) used to be a black box. One bad row could sink the whole batch, admins had no idea how far along it was, and a big job could hog the queue and hold up everything else.
Over eight small phases, each one shippable on its own, I isolated failures per learner, gave big jobs their own queue, batched the database writes and added a live progress bar with a per-learner results summary. Now it’s boring, which is exactly what you want.
The full story →
- Beanstalkd
- PHP
- Angular
- Performance Metrics
A whole new look for learners
I led a months-long redesign of the main learner theme (navigation, dashboard, account pages and storefront) that touched roughly 700 files. Along the way I pulled a shared AngularJS component library out of a lot of copy-pasted markup, tidied colors, spacing and type into a real design system, moved 100+ files over to Phosphor Icons, and swapped hard-coded text for translation keys.
Under the hood I rebuilt the Webpack config (vendor splitting, custom chunks, slimmer locale data) and updated the DI-annotation tooling so ES6-class components survive transpilation.
Plugging security holes
I found that a deactivated user could bring their own account back to life just by asking for a password reset. Oops! The fix touched both apps: new checks in the API, revoked tokens, and sessions that close the moment someone is deactivated.
I also toughened up self-registration with domain-based blocking, reCAPTCHA / Cloudflare Turnstile and required email confirmation before an account goes live.
- Security Audits
- PHP
- SSO/Identity
Data that heals itself
A bunch of long-standing bugs turned out to be data quietly drifting out of sync. Learning paths never auto-completed if a course was added after someone enrolled. Deactivated users still showed up as “active” because two status fields disagreed.
Instead of a migration and a pile of manual cleanup, I added lightweight checks that spot the drift and fix it when the page loads, at basically no cost. When the cause was structural (deleting a learning path never cleaned up its course links), I fixed the cascade itself, and that whole family of bugs just went away.
- Data Architecture
- PHP
- Incident Response
Reports people actually want
A multi-district education client was building their monthly reports by hand in Airtable. I built them a reporting system with saved, filtered views; a CSV export laid out district → school → course → module → learner to match their spreadsheet exactly; and scheduled delivery, so every principal and district rep automatically gets just their own data each month. No more report day!
- Data Architecture
- PHP
- Requirements Gathering
Goodies for course authors
One-click copies of assessments, surveys and resource libraries (questions and cross-references included). Asset tracking that shows exactly which courses use a file, so stale ones can finally be deleted. A trackable document-link block. A compact certificate picker. And a theme manager that had lived in the codebase for years without ever actually working. It works now!
Integrations
An in-app Zoom webinar player built on the Meeting SDK, a Vimeo upload flow, a third-party single sign-on provider (plus reconciling and backfilling member identity records), and a Node.js CLI for importing and reconciling a partner’s member and credit data.
- Third-Party API Integration
- SSO/Identity
- Node.js