Luxury Retail Clienteling Platform
Overview
A cloud-native, multi-brand clienteling platform built for one of Europe's largest luxury goods conglomerates. The platform is used by Client Advisors (CAs) — in-boutique sales staff — across several luxury Maisons to manage and deepen long-term relationships with their most valuable clients. Think of it as a CRM meets a luxury concierge app: every phone call, WhatsApp message, store visit, or gifting moment gets logged, every client gets a 360° view, and every CA knows exactly where a relationship stands.
The backend is a microservices system of 18 services running on GCP Kubernetes, following an inner source model where LVMH's platform team publishes shared NestJS libraries that each brand team adopts and extends independently.
| Domain | Luxury Retail / Clienteling (B2B2C) |
|---|---|
| Role | Senior Backend Engineer (Consulting) |
| Team | 6+ Engineers across platform & brand teams |
| Stack | NestJS, TypeScript, Salesforce, GCP, Kubernetes, Istio, FluxCD, Redis, Elasticsearch, Firestore, Adyen |
| Scale | 18 microservices · 93 API endpoints · 10+ interaction types · multi-brand deployment |
System Architecture
The platform is a microservices architecture deployed on GCP GKE. All external traffic passes through an Istio ingress gateway that enforces TLS termination and JWT validation. Services communicate internally via Kubernetes DNS secured by Istio mTLS. Salesforce is the primary system of record for all CRM data.
Service Catalog
| Service | Domain | Key Integrations |
|---|---|---|
client360 | 360° client view: header, KPIs, timeline | Salesforce, other microservices |
outreach | Interactions, templates, newsletters, outreach channels | Salesforce |
clients | Client profile CRUD and search | Salesforce, Redis, Bynder |
users | CA profiles, store assignments, widgets | Salesforce, Appointments svc, Tasks svc |
appointments | Appointment booking | Salesforce, Booxi, Bambuser |
consultation | Virtual and in-store consultations | Salesforce, Bambuser, SightCall, Redis |
curations | Personalized product sets | Salesforce, Firestore, GCS |
orders | Pay-by-link orders | Salesforce, Adyen, Redis |
transactions | Purchase history | Salesforce, SFCC (Demandware) |
products | Product catalog search | Elasticsearch, Redis |
tasks | CA task management | Salesforce |
events | Brand events | Salesforce |
dashboards | CA performance dashboards | Salesforce, GCS |
notifications | Push notifications | Firebase, Firestore |
nps | Net Promoter Score surveys | Salesforce, Transactions svc |
translations | UI translation strings | Salesforce, GCS |
Inner Source Model
The most distinctive architectural decision in this project is the inner source model — applying open-source contribution principles to internal platform libraries. The LVMH platform team maintains shared NestJS libraries that each brand team installs as versioned NPM packages and extends with brand-specific implementations.
Each inner source repository is a NestJS monorepo with two layers:
lvmh-svc-outreach/
├── apps/
│ └── lvmh-svc-outreach/ # Reference implementation (not published)
│ └── src/
└── libs/
└── is-svc-outreach/ # Publishable NPM library (is- = inner source)
└── src/
├── interaction/
├── newsletter/
├── outreachChannel/
└── template/
Why inner source over a shared microservice?
| Approach | Shared Microservice | Inner Source Library |
|---|---|---|
| Blast radius | A bug affects all brands simultaneously | Each brand runs its own isolated binary |
| Upgrade cadence | Forced — all brands on same version | Each brand upgrades at their own pace |
| Brand customization | Hard — feature flags proliferate | Clean — brand overrides specific layers |
| Operational overhead | One shared deployment to maintain | N brand deployments, but fully autonomous |
Plugin Architecture — Extensibility Without Forking
I contributed to designing and implementing the plugin/registration system inside the inner source outreach library. The OutreachCoreModule.register(config) pattern lets any brand replace any layer (controller, repository, mapper, interaction processor) using NestJS dependency injection — without touching the core code.
A brand's AppModule looks like:
OutreachCoreModule.register({
interactionFindAllRepository: {
provide: INTERACTION_FIND_ALL_REPOSITORY,
useClass: BrandCustomInteractionRepository,
},
interactionProcessors: [BrandWatchDeliveryProcessor],
})Everything not overridden falls through to the library's default Salesforce implementation.
Outreach Service — Deep Dive
The Outreach service is the domain I worked on most deeply. It manages every recorded touchpoint between a CA and a client: calls, messages, store visits, product shares, consultations, and more.
Interaction Type System
Every interaction has a context discriminator field that determines which processor handles enrichment when the interaction is read back. This is the core abstraction that allows interactions to carry typed, context-specific metadata.
Interaction Channels
Call · SMS · Email · WhatsApp · WeChat · Line · LineWorks · Kakao · Postal · Action
Salesforce Storage Model
Interactions are stored using Salesforce standard and custom objects:
Create Interaction Flow
Read Interactions with Enrichment
API Versioning Strategy
The platform manages breaking changes through explicit API versioning in the URL path:
| Version | Status | Response Format |
|---|---|---|
v1 | Deprecated, kept for compatibility | JSend wrapper: { status, data: { result } } |
v2 | Current (most endpoints) | Raw JSON payload |
v3 | Latest (inner source library) | Raw JSON, full RESTful CRUD |
V1 endpoints are implemented as thin wrappers that call V2 internally and wrap the response — no duplicated business logic.
Contract-First Development
All APIs are defined in OpenAPI YAML specs before implementation. Models are generated from the contracts, and runtime validation enforces the spec on every request and response.
Each brand maintains its own contracts submodule. This means brands can enable or disable features simply by including or excluding endpoints from their spec. The generated TypeScript models and the runtime validator both derive from the same source of truth.
Authentication & Authorization
Key design choices:
| Decision | Rationale |
|---|---|
| JWT validation at Istio, not in services | Services don't need to implement auth; one place to update if Okta config changes |
TrustedJwtAuthGuard | Trusts that Istio already validated the token; extracts CA identity from forwarded headers — no redundant crypto overhead |
| OAuth 2.0 PKCE for mobile | Prevents authorization code interception attacks; no client secret needed in the mobile app binary |
| Istio mTLS between services | Zero plain-text traffic inside the cluster; service-to-service calls are mutually authenticated without application code changes |
Infrastructure & GitOps
The entire deployment lifecycle is managed through GitOps using FluxCD. Git is the single source of truth for what runs in every environment.
Environments
| Environment | Git Branch | Trigger |
|---|---|---|
dev | develop | Auto-deploy on every merge |
staging | main | Auto-deploy on merge to main |
production | git tag vX.X.X | Manual promotion via tag |
sandbox | feature branch | Per-developer sandboxes |
Per-Service Kubernetes Resources
Each of the 18 microservices is deployed with:
- Deployment — multi-stage Docker build (Node alpine),
node dist/main - HPA — auto-scales from 2 to 5 replicas on CPU/memory
- ExternalSecret — pulls credentials from GCP Secret Manager; zero secrets in Git
- Istio VirtualService — per-route JWT auth policies and traffic rules
- CronJobs —
product-catalog-sync(nightly) andtranslations-compilation(hourly)
Tech Stack
| Layer | Technology |
|---|---|
| Backend framework | NestJS (Node.js, TypeScript) |
| Primary data store | Salesforce CRM (SOQL via jsforce) |
| Caching | Redis (ioredis) |
| Product search | Elasticsearch |
| Real-time / events | Firestore, Firebase Cloud Messaging |
| Payments | Adyen |
| Video / appointments | Bambuser, Booxi, SightCall |
| Auth | Okta (OAuth 2.0, PKCE), Istio JWT validation |
| Service mesh | Istio (mTLS, JWT policies, traffic management) |
| Container orchestration | Kubernetes (GKE), FluxCD GitOps |
| Secrets | GCP Secret Manager + External Secrets Operator |
| CI/CD | GitHub Actions (lint, test, Docker build, NPM publish) |
| API validation | express-openapi-validator, openapi-generator-cli |
| SF batching | sf-composite-call |
| Logging | cloud-pine, nestjs-pino (structured JSON) |
Retrospective — What I Would Do Differently
1. Event-Source Interactions from Day One
What we have: Interactions are Salesforce Task records — mutable, overwritten on update.
The problem: No audit trail. When a timeline entry ended up in an unexpected state, debugging meant correlating Salesforce logs and service logs across the async enrichment pipeline.
What I'd do now: Event-source the interaction aggregate. Emit InteractionCreated, InteractionValidated, InteractionDeleted events and derive current state from the stream. Trivial audit log, time-travel debugging, and unlocks event-driven downstream consumers (analytics, recommendations).
2. Decouple Enrichment from the Read Path
What we have: When a CA reads their interaction timeline, the service synchronously calls Products, Curations, and Orders services to enrich each interaction — coupling read latency to N downstream service response times.
The problem: If Products is slow, the entire timeline is slow. Under load, this amplifies tail latencies.
What I'd do now: Enrich interactions at write time, not read time. When an interaction is created, publish an InteractionCreated event. An async enrichment worker resolves the metadata and writes it back. The read path is then a single Salesforce query — fast and isolated.
3. Consumer-Driven Contract Testing
What we have: Brands' OpenAPI contracts are validated at runtime, but there are no automated contract tests between the mobile app and the backend services.
The problem: A backend service can change a response field, the contract gets updated, but the mobile app — which hasn't updated yet — breaks silently in production.
What I'd do now: Implement consumer-driven contract testing (Pact). The mobile app publishes what it expects from each endpoint. CI fails on the backend if a change would break any registered consumer — before it ever reaches staging.
4. GraphQL or BFF for the 360° View
What we have: The client360 service orchestrates calls to 6+ downstream services, assembles the response, and returns one big payload.
The problem: The mobile app sometimes only needs a subset of the 360 view (just the KPI cards, or just the timeline). But it always gets the full payload, wasting bandwidth and increasing cold-start latency on the app screen.
What I'd do now: A lightweight Backend for Frontend (BFF) layer or a GraphQL gateway that lets the mobile app request exactly the fields it needs. Reduces payload size, enables parallel field resolution, and makes the client360 service stateless and composable.
5. Observability Before the First Deploy
What we have: Structured logging (pino) was added, but distributed tracing across the enrichment pipeline and cross-service calls was an afterthought.
The problem: A slow interaction read involves 4+ services. Finding which hop added latency required manually correlating trace IDs across separate log streams.
What I'd do now: OpenTelemetry instrumentation from day one — automatic trace propagation through HTTP calls and Salesforce composite calls. A single Jaeger/Cloud Trace view showing the full request waterfall, with per-hop latency highlighted immediately.
Impact & Key Metrics
- Delivered across 18 microservices with 93 API endpoints serving multiple luxury brands simultaneously
- Designed and implemented the interaction type processor system — a plugin architecture supporting 12 distinct interaction contexts with zero coupling between types
- Built the inner source library contribution pipeline: from monorepo structure to versioned NPM publication and brand-side integration
- Implemented Salesforce composite call batching — reducing a 5-round-trip interaction create to a single HTTP call to Salesforce
- Architected multi-brand support where each Maison independently controls its feature set via OpenAPI contracts — enabling or disabling features without touching shared code
- Platform runs with HPA auto-scaling (2–5 replicas per service) and zero secrets in Git via GCP Secret Manager + External Secrets Operator
Built for the people who make luxury feel personal.