Skip to main content
YK logo
← Back to projects

Triple CMS — Publisher Configuration Platform

Enterprise CMS for managing multi-publisher layouts, offer configurations, and approval workflows

Triple CMS publisher configuration builder interface

Overview

Triple CMS is an enterprise content management system for managing publisher configurations across Triple's offer network. Publishers use it to build and publish the layouts, themes, and offer selections that drive their end-user experience — from hero banners and colour schemes to which financial products appear and in what order.

My role covered the React frontend of a full-stack monorepo: building the builder interface, the version management system, and the component library that underpins the whole application.


The problem

Managing configurations for multiple publishers with different brand requirements, content teams, and approval processes is messier than it sounds. Configuration changes were fragmented with no shared approval workflow, no revision history, and no way to preview what a change would look like before it went live.

The CMS needed to solve three things at once: a visual editor flexible enough to handle different publisher layouts, a version control system with proper review states, and enough access control to let the right people approve or block changes without slowing the whole process down.


The application

Page builder

The core of the product is a page builder: a visual interface where publishers configure sections, adjust themes, manage hero images, and control offer display logic. State is complex here — the user's in-progress configuration lives in Redux while committed versions are fetched and cached via React Query. Keeping those two in sync without collisions took some care.

Configuration

Redux Toolkit handles configuration state (theme colours, header layout, offer ordering) because that state is global, deeply shared across the builder interface, and changes frequently as the user edits. React Query handles everything that comes from the API: versions, comments, publisher data. The split was intentional. Mixing remote server state and local UI state in Redux alone would've made cache invalidation painful and the edit experience sluggish.

Version control and approval workflow

Every configuration change creates a revision. Revisions move through states: draft, pending review, approved, published. Team members can comment on a version, request changes, or approve it. Only approved versions can be published.

Featured Pages

This was the most involved part of the frontend. The version management UI needed to surface the current state clearly, make transitions obvious, and handle the edge cases — what does "pending review" look like when the reviewer is also the author? What happens when an approved version gets superseded by a new draft before it goes live?

OpenAPI-first client generation

The backend is FastAPI with an OpenAPI spec as the source of truth. The TypeScript API client is generated from that spec at build time. The frontend never drifts from the API contract — when the backend adds a field or changes a response shape, the generated types surface every affected place in the frontend at compile time.

It's the kind of setup that's invisible when it's working and a nightmare when it's not. Having it right meant iterating on the API without the usual back-and-forth of "wait, what does that endpoint actually return in production?"

Component library

60+ components, documented in Storybook, with CSS Modules for scoped styling. Building a library of this size inside a product (rather than as a standalone package) creates a constant tension: components need to be general enough to reuse, but the temptation is always to make them specific to the context at hand. The Storybook requirement helped — having to write a story forces you to think about a component's interface in isolation.


Engineering decisions

Redux + React Query — not either/or

The state management split trips people up at first. Redux holds UI configuration state that changes as the user interacts with the builder. React Query holds server state, fetched, cached, and invalidated around API calls. The way to think about it: if data comes from the server and needs caching and background refetching, React Query. If it's ephemeral UI state driven by builder interactions that doesn't map to an API resource, Redux.

Using Redux for everything would've meant writing manual cache invalidation. Using React Query for everything would've meant wrestling with mutations for UI state. The split is worth the complexity once the app is past a certain size.

E2E testing with Playwright

Playwright tests cover the main user flows across multiple browsers and devices via BrowserStack. The version management workflow was particularly hard to test meaningfully in isolation — the interesting behaviour happens across multiple interactions in sequence. E2E tests made it possible to verify that a full publish cycle worked correctly without someone manually clicking through it on every release.

Monorepo with shared backend libraries

The backend is FastAPI with Python, alongside the frontend in the same repo. Shared libraries (db-models, shared, cms-jwt-token) are used across several Lambda microservices. Infrastructure runs on Terraform and Terragrunt across dev, QA, sandbox, and production. The split between serverless Lambdas for lightweight services and ECS containers for the main API gave the right tradeoffs for different load profiles.


Stack

Layer Choice
Framework React 18 · TypeScript · Vite
State Redux Toolkit + TanStack React Query v5
Routing React Router 6
Styling CSS Modules · Tailwind CSS
Components Custom library · Storybook
Backend FastAPI (Python) · Connexion · PostgreSQL
Auth AWS Cognito · JWT
Infrastructure AWS Lambda · ECS · Terraform
Testing Playwright · BrowserStack · Pytest
Monitoring Datadog

Reflections

The version control workflow was the most interesting part to build. The technical implementation wasn't especially complex — the hard part was making the UI accurately represent a process involving multiple people and multiple states. Too rigid and teams route around it. Too loose and the approval flow means nothing.

The OpenAPI code generation is a good reminder that the tooling you set up early shapes how fast you can move later. More upfront work, but the payoff was real: the frontend stayed in sync with a backend that was actively evolving, and a whole class of runtime error stopped happening.