Skip to main content
YK logo
← Back to projects

Triple OMS — Production Offer Management System

Triple OMS publisher dashboard showing offer listings and management controls

Overview

Triple OMS is a publisher-centric Offer Management System — a live commercial platform that manages thousands of financial offers (credit cards, loans, savings products) across multiple publisher tenants. My role was entirely on the frontend: building and iterating on the publisher-facing React application that publishers use daily to manage offer listings, resolve data issues, and track offer status across their tenant.

This isn't a side project or a proof-of-concept. It's deployed across dev, QA, sandbox, and production environments with real publishers relying on it for real financial product data. The frontend sits at the end of an event-driven AWS pipeline — and understanding that pipeline was necessary to build the UI well.


The Frontend Challenge

Building the publisher-facing UI for a multi-tenant offer platform introduces problems that a typical CRUD app doesn't. Offers flow in near-real-time from an upstream service via Kinesis and TypeSense; the UI needs to represent data that is sometimes in-flight, sometimes partially indexed, and occasionally out of sync. Publisher tenants share the same system but must see only their own data. Search needs to work across thousands of records without degrading the experience.

The challenges this creates for the frontend:

  • Stale data is a real risk — a publisher viewing an offer might be looking at a version that hasn't caught up with a recent upstream change
  • Loading and error states matter more than usual — the pipeline has multiple stages where things can lag or fail, and the UI needs to communicate that clearly
  • Multi-tenancy must be invisible — tenant isolation happens at the API level, but the UI has to be built with that model in mind at every layer, from auth tokens to query keys
  • Search UX has to feel instant — publishers work across thousands of offers; a search that lags or returns stale results erodes trust quickly

Triple OMS offer management view


The Application

The publisher-facing UI is a React 19 / TypeScript SPA built with Vite. The key architectural choices:

TanStack Query v5 for all server state

Every piece of remote data in the application goes through TanStack Query. This was the right call for a system like this — the library's approach to stale-while-revalidate, background refetching, and cache invalidation maps well onto a platform where data freshness is genuinely variable.

Query keys are structured to include tenant context, so cache entries are naturally scoped and never bleed across tenants. On actions that mutate offer data, targeted invalidations trigger refetches of the affected queries — the UI stays consistent without polling or manual refresh logic.

// Query keys are tenant-scoped throughout
const offerKeys = {
  all: (tenantId: string) => ['offers', tenantId] as const,
  list: (tenantId: string, filters: OfferFilters) => [...offerKeys.all(tenantId), 'list', filters] as const,
  detail: (tenantId: string, offerId: string) => [...offerKeys.all(tenantId), 'detail', offerId] as const,
};

Error boundaries combined with TanStack Query's error state keep failure handling consistent and avoid the scattered try/catch pattern that tends to spread through components when state management is ad hoc.

React Router 7

Navigation is handled by React Router 7. Tenant context flows through the URL structure — publishers land on routes scoped to their tenant, and the router ensures that switching context triggers the right data transitions rather than stale renders.

TypeScript discipline

The codebase runs under strict TypeScript, ESLint at zero warnings, and Prettier enforced on every commit. Working over an API that returns multi-tenant, heavily-typed offer data — with distinct shapes for different offer categories — makes that discipline essential rather than optional.

API response types are defined once and imported throughout. When the backend adds or changes a field, TypeScript surfaces every place in the UI that needs to handle it. The alternative — making loose assumptions about response shapes — is a category of bugs that's surprisingly easy to introduce and surprisingly hard to track down in production.

Auth with AWS Cognito

Auth is handled via AWS Cognito, with tenant-scoped tokens. The frontend manages token lifecycle transparently — refresh, expiry, and per-request injection are handled at the HTTP client layer so component code never needs to think about it.


Engineering Decisions Worth Talking About

Structuring queries around tenancy

The multi-tenant model shapes every query in the application. Early on, it would have been easy to structure queries without tenant context in the key — and then discovered later that cached data from one tenant was surfacing for another after a context switch.

Building tenant context into query keys from the start meant cache isolation was structural, not something enforced by convention. It also made query invalidation after mutations straightforward — invalidate ['offers', tenantId] and everything downstream updates correctly.

Keeping components thin

TanStack Query does the heavy lifting on data lifecycle, which makes it easy to keep components focused on rendering. The pattern throughout the codebase: hooks handle fetching and mutation logic, components receive data and fire callbacks. This separation makes components easier to reason about and easier to test in isolation.

Triple OMS offer details view

Handling pipeline latency in the UI

The backend pipeline introduces latency between upstream changes and what the API returns. Rather than pretending this doesn't exist, the UI surfaces it where it matters — showing last-synced timestamps on offer records, distinguishing between "loading" and "refreshing" states visually, and giving publishers clear feedback when an action has been submitted but the downstream effect hasn't propagated yet.

This required understanding the pipeline well enough to know which operations were synchronous (API writes) and which were eventually consistent (search index updates, cache invalidation). Getting that wrong produces UI that lies to the user.


Frontend Stack

Layer Choice
Framework React 19 · TypeScript · Vite
Server state TanStack Query v5
Routing React Router 7
Auth AWS Cognito (user + M2M tokens)
API FastAPI (Python) · Pydantic v2
Search TypeSense (via API)
CI/CD GitHub Actions
Monitoring Datadog

Reflections

Working on the frontend of a production multi-tenant system surfaced something that greenfield projects rarely do: the gap between what the UI says and what the system is actually doing.

When a publisher searches for an offer, the result depends on a search index staying in sync with a database. When a page loads slowly, the bottleneck could be a TanStack Query configuration, an API response time, or something that happened three steps upstream in the pipeline. Getting the frontend right meant understanding enough of the system to know which category a given problem fell into.

The other takeaway was how much tooling culture shapes frontend quality. Strict TypeScript, zero-warning ESLint, and Prettier enforced on every commit aren't constraints — they're the reason you can confidently refactor a component without worrying that something three files away has silently broken.

"The UI is only as good as your understanding of what's behind it."