← IndexCase studies — 01 / 04Live site ↗
Health platform · consolidationAug 2023 — ongoing

Several sites,one platform.

Possums helps exhausted parents and the clinicians who support them. The knowledge was excellent; finding it was not. The parent programs sat on one domain, the marketing that explained them on a second called Dr Pam, and the professional training on a third, NDC Institute. Marketing made the call to bring everything under possums.org, for SEO and a single brand. I have worked across the platform throughout: the programs app, the shared component and type packages the other apps build on, and the API and schema behind them. Dr Pam moved across last year; NDC Institute is next, and that is the part I am building now.

Client
Possums
My role
Fullstack developer
Team
Design, product, engineering
Timeline
Aug 2023 — ongoing
Possums — screenshot

01 — The problem

The content lived across separate websites, each with its own navigation, styling and login. A parent who arrived through the marketing site had to cross to a different domain to reach the programs it described, and a parent at 3am with an unsettled baby had to guess which site held the answer. A clinician looking for training had the same problem in reverse.

Two very different audiences were being served by one voice, and neither journey was clear. Maintaining separate codebases also meant every change had to be made, and tested, several times over.

The real constraint

The users are sleep-deprived. Every extra click, every ambiguous label, every slow page is a person giving up on help they need.

02 — My role

Fullstack developer, working alongside the design and product teams. Most of my work is on the front end, including the shared component and type packages the other apps depend on, but a fair amount sits behind it: NestJS endpoints, Prisma schema changes, GraphQL, and data migrations against live records. Most recently I worked on a subscription model change, replacing short plans with multi-year ones, which meant keeping every existing subscriber working while the model shifted underneath them.

The subscription model migration: legacy product support, upgrade paths, and the entitlement rules that decide who still has access

Access control around the paywall, including module prerequisites enforced on the server and blocking LLM crawlers from reading paid programme content

Editorial tooling: a draft and publish workflow for resources with YAML frontmatter, article and programme tagging, and a custom markdown directive for pull quotes

Shared components the organisation's other applications are built on, including the dropdown and tag inputs that replaced the old ones everywhere

Search for the practitioner directory, from the debounced autocomplete down to the endpoints behind it

Newsletter signup through Klaviyo, routed via the API with typed audience properties

Learning features: article progress tracking, quiz entry points and dashboard progress

Models, migrations and schema changes in Prisma, run against live records

Performance and accessibility: route prefetching, lazy loading, WCAG AA contrast and keyboard paths

Problems I found and fixed

03 — bugs I caught, and what I changed

Product direction came from the team and the architecture calls from my lead. These are problems I found in the code and the fixes I shipped.

01

Draft articles were publicly readable

Prisma · data integrity

The blog listing queried articles without filtering on publish state, so anything saved as a draft was reachable by anyone who found the URL. I added the constraint at the query level in the article service rather than filtering in the component, so it holds everywhere that service is called.

Trade-offA query-level filter means any future preview mode has to opt out explicitly. Worth it, because the safe default should be "not published".

02

Sign-ins failed on capitalisation

Prisma · auth

Anyone who registered with a capitalised email could not log back in with the lowercase version, because the lookup compared the stored string exactly. Fixing it properly took three changes rather than one: a case-insensitive lookup so existing accounts could sign in, normalisation on write so new ones are stored consistently, and an admin-only migration endpoint to clean up the rows already in the database.

Trade-offMy first migration read every user and updated the mismatched ones row by row. That is one round trip per user for something the database does in a single statement. I replaced it with a raw SQL update three days later. Slower to write twice, far faster to run, and much less to go wrong halfway through.

03

The page stopped scrolling

React · lifecycle

The article Progress Tracker locked body scroll while its sidebar was open, but never removed the class when the component unmounted. Navigating away left the whole page unscrollable. I added the cleanup to the effect and reset the sidebar state with it.

Trade-offNothing is traded here. It was a missing cleanup, not a design choice. It did change how I work: anything added to the body now gets a paired removal in the same effect.

04

The carousel could never reach its last card

React · shared hook

The guest speaker carousel tracked its position with a round of scrollLeft divided by the track width. Cards are sized as a percentage of that track, so the estimate drifts further out with every item, and the last card's left edge never reaches the viewport's, so it could not be selected at all. I measured position off the items themselves, special-cased the end of the scroll, and moved the whole thing into the shared carousel hook, which took 68 lines out of the component. While I was there I hid the dots and arrows when there is nothing to scroll, using a ResizeObserver so it stays correct as the layout changes.

Trade-offMeasuring per item is a little more work on each scroll than a single division. It is correct at any card width, which the division never was.

04 — How it’s built

Front end
Next.js · TypeScript · Sass modules
API
NestJS · GraphQL
Data
PostgreSQL via Prisma
Content
Editor-managed pages, resources and courses
Search
Meilisearch, indexed from the article service
Quality
Semantic HTML, keyboard paths, Lighthouse budgets

05 — Accessibility

Health information has to reach everyone, including a parent one-handed on a phone in a dark room. Treated as a requirement, not a polish pass.

Semantics first
Real headings, landmarks and lists, so screen readers and skim-readers get the same structure.
Keyboard paths
Every interactive element reachable and visibly focused, with no mouse-only journeys.
Contrast & type
Readable sizes and contrast held to WCAG AA, checked in the component library rather than per page.
One-handed use
Mobile-first layouts and generous tap targets, for the parent holding a baby in the other arm.

06 — Outcomes

Outcomes for the platform as a whole, built by a team over several years. What I worked on is described above.

Organic search
Sessions grew 37% year on year, from 14,494 to 19,835, and organic overtook direct to become the largest channel at 47% of all traffic.
Not bought
Paid search was reduced to almost nothing across the same period, from 9,444 sessions to 19, so none of that growth came from advertising.
Organic social
Up 68%, from 1,247 to 2,100 sessions, almost all of it Instagram and none of it promoted.

07 — What I’d do next

Audio is moving onto the site. A lot of Possums is meant to be listened to rather than read, by someone who cannot be looking at a screen, and it currently lives elsewhere. Hosting it ourselves makes the player, the storage and the progress tracking all ours to build.

The other piece is making Possums installable. A progressive web app is the sensible route: one codebase rather than two native builds, and it answers the audio problem at the same time, because a service worker can cache episodes for offline listening. That matters for this audience specifically. A parent settling a baby at 3am is not reliably on wifi.

← Indexderoubaix.marisha@gmail.comMarisha Deroubaix — 2026