Projects / Microlearning / Overview
Microlearning
Lifeed's learner-facing PWA — bite-sized, CMS-authored modules for 50k+ learners, rendered from content whose shape was never finalized, behind a Go gateway that keeps every credential off the browser.
- Year
- 2024
- Role
- Full-Stack Engineer
- Focus
- Frontend · Backend
- Status
- Live
Problem
Microlearning is the learner-facing half of Lifeed's platform — an installable PWA delivering bite-sized, CMS-authored modules to more than 50,000 learners: sessions, reflective missions, journaling, achievements and certificates, reminders.
Two things make it harder than a set of screens. A single learner view needs data from roughly eight separate internal services — progress, profiles, client entitlements, assessments, identity, notifications — each with its own auth and response shape. And the content itself is authored as open-ended, typed blocks whose set was never finalized: new types kept arriving, designs for them shipped continuously, and the frontend had to be running before most of them existed.
I was responsible for the frontend. The gateway described below is the boundary it was built against, and the reason the client stays as simple as it does.
Approach
- The frontend is a parse engine, not a set of layouts. Content arrives as trees of typed blocks and cards; a registry maps each type to a parser producing normalized props for a Vue component, and the walker recurses through nesting by dispatching children back through itself. Adding a kind of content is adding a parser and a component — the pipeline never changes.
- Unknown types are skipped, not thrown. That single behaviour is what decouples the content team's publishing from the frontend's release cycle. Without it, an author publishing a block the deployed client has never seen doesn't produce a gap, it produces a broken screen mid-session. The frontend's promise becomes "I skip what I don't recognize" rather than "I recognize everything, forever."
- A Go Backend-for-Frontend is the only thing the browser talks to — one composed response instead of eight calls, and every downstream credential held server-side rather than scattered into client JavaScript.
- Auth is a cookie session at the edge, a JWT only upstream. The browser holds an HttpOnly, signed-and-encrypted session cookie plus a CSRF token, never a bearer token. Token verification, context resolution and refresh all live behind the boundary; the client's entire auth footprint is retrying once when a request comes back unauthorized.
- Localized copy comes from the design tool, not the database. Strings previously lived as per-locale rows an engineer had to edit with a content creator present. Moving them into Ditto, attached to the Figma frames they appear in, meant a script at the BFF fetches and caches them — and publishing became the act that makes text live, with no engineer in the loop.
- Delivered as an installable PWA because the product needed to send notifications. Completion rates depend on learners coming back for the next lesson, which means reminders on a phone. Web push requires a service worker, and on iOS it requires the app to be installed to the home screen — so installability isn't a nicety on top of the feature, it's the delivery mechanism. One Vue codebase covering iPhone, Android and desktop, against the alternative of two native apps built to send a message.
Results
- A content model the CMS team extends without a frontend rewrite — new block, card, and wizard types are additive, and an unrecognized type degrades to a gap rather than breaking a learner's session.
- One session-based API in front of a fleet of internal services: a single origin for the PWA, one auth model to reason about, and no upstream secret ever reaching the browser.
- Powers the sessions, journaling, reflective-mission and achievement experience for 50,000+ learners, installable and push-capable across iOS, Android and desktop from a single codebase.
Lessons
- A weaker contract is sometimes the stronger design. The frontend couldn't enumerate content types that hadn't been designed yet, so it promised something smaller and kept it — which is what let content and code ship on independent cadences for years.
- Skipping silently has a blast radius worth measuring. An unknown leaf costs one widget. An unknown nesting type costs everything inside it, because the walker never descends into children whose parent it didn't recognize. Rendering an unstyled passthrough instead of skipping would keep the content and lose only the layout — the better trade, and one I'd make now.
- Keeping the browser away from bearer tokens is a posture, not a detail. An encrypted HttpOnly cookie can't be read out by injected JavaScript the way a token in JS storage can. It doesn't stop a script acting as the user while the page is open — but it stops a credential leaving with it.
- Platform requirements decide architecture more often than architecture preferences do. Nobody chose a rendering strategy here. They chose to be able to remind someone about lesson four, and the rest followed.
Where it stands
This is a 2024 system, and it's worth being clear about where the engineering is strongest and where I'd invest next. The parse engine and the security-at-the-edge model are what I'd keep untouched. The maturity I'd add is operational:
- Test against real dependencies, not doubles. Mocked downstreams mean a contract drift upstream only surfaces at runtime. Integration tests against contract-faithful downstreams — plus table-driven coverage on the parse engine, which degrades silently by design — are the highest-value gap.
- Observability beyond health checks. For a gateway whose health is dominated by the services behind it, per-downstream latency and error metrics turn "something's slow" into "this dependency is slow."
- Telemetry on skipped blocks. A design that swallows unknown types depends on someone noticing them. A log line nobody reads is functionally the same as no log line.
- Resilience budgets. Per-dependency timeouts and circuit-breaking, so one slow upstream can't pin the whole gateway behind a shared ceiling.
Much of that is discipline I leaned into harder on the Internal Admin Tool eighteen months later — real-database testing as a merge gate, invariants pushed into the data layer — and it's the direction this system would grow.