Imprint
Problem
A customer-facing servicing app for white-label credit products had to support many partners simultaneously — different theming, different capabilities, different copy, different flows — from a single deployable codebase. New partner onboarding and per-partner iteration had to happen without shipping partner-specific branches in the app code, and every change had to be safe to roll out to live card-holders.
Constraints
Partner behavior was API-driven, so shortcuts like hardcoded product IDs or merchant-key if branches were forbidden (and enforced by custom lint rules). Redux state spanned dozens of persisted slices that survived logout, so any state change had to reason about persistence semantics, not just current-session behavior. All user-facing strings had to live in a shared translations package with EN/ES parity — inline literals were rejected in review. The internal design system was being adopted in parallel with feature work, so most primitives had a v1 and v2 form coexisting, and new work had to consume the DS while occasionally contributing missing pieces back.
Approach
Replaced partner conditionals with a capability-based ExperiencePolicy pattern so gating happened via booleans derived from server config, not from product IDs. Standardized deeplink handling behind a ProtectedRoute wrapper that preserved search params through the login round-trip, so limited-servicing tiers and success routes worked identically for every partner. Centralized analytics behind a single event enum and mirrored errors to Datadog RUM with sourcemap uploads, so production stack traces resolved cleanly to original code. Built a compound-engineering loop (plan → work → review → compound) where solved problems became searchable docs, and codified recurring lessons as ESLint rules or test patterns so they couldn't drift.
Outcome
Partner rollouts became config-driven rather than code-driven — most partner-specific behavior tuning stopped touching React entirely. Cross-cutting bug classes (auth gating on deeplinks, stale partner-config caching, dark-mode token misuse, translation drift) moved from post-merge fires to lint-time or review-checklist catches. Median PR size stayed small enough that same-day merges became the norm for the 100–300 line range, and observability + workflow scaffolding shrank the loop between "bug reported" and "fix deployed" — most triage started from a real stack trace instead of a local repro.
Tech
- Claude Code
- Datadog RUM
- Design System
- ESLint (custom rules)
- Playwright
- pnpm workspaces
- React
- Redux Persist
- Redux Toolkit
- Statsig
- Storybook
- Testing Library
- Turborepo
- TypeScript
- Vite
- Vitest