Solution Architecture  /  Delivery  /  Digital Experience Platform

An idea on a Thursday.
On phones 27 days later.

Staff picks let a museum's own people tell visitors what to look at, and why. The idea came from our leadership team. A working prototype with real collection objects and live wayfinding was in production the same afternoon. The feature was authored in a headless CMS, published through an integration hub, and built into a React Native visitor app by four teams in about thirty hours, then landed on Apple TestFlight and the Google Play internal track ahead of the app's public store release. This is how we did it, and how we do it again.

24 SEP · 12:12 contract proposed 24 SEP · 14:33 prototype in prod OCTOBER review → staff picks 20 OCT · 14:40 CMS schema live 21 OCT · 17:01 app screens merged 21 OCT · 17:52 iOS + Android builds NOT TO SCALE · ONE CONTRACT FROM PITCH TO PHONE

Every museum is full of people who know which object in the room has the best story. A conservator knows the repair nobody notices. A host knows the thing children always stop at. A curator knows why an unremarkable piece of terracotta is in the case at all. Visitors almost never hear any of it, because the app only knows what the catalogue knows.

Staff picks are our attempt to fix that: profiles of real staff told as questions and answers, one set per venue, where each answer can point at real collection objects, events, shop products and links, with a way to walk you to the object. This post is less about the feature than how it was delivered: from a pitch on 24 September to iOS and Android builds on 21 October, through review and CI the whole way, across six repositories and four teams. I think the delivery pattern is the part worth keeping.


01 · THE PROBLEMAn idea with five owners

On paper a staff-picks feature is small: a list of people, a page per person, some linked objects. In practice it touches nearly every layer of a digital experience platform, and each layer has a different owner. Editorial content lives in a headless CMS. Published data reaches the platform through an integration hub behind an API gateway. The visitor app is a React Native app with its own .NET backend. Collection records, events and wayfinding come through a Content API. Releases go through a mobile pipeline that every feature shares.

A feature that crosses five owners has a predictable failure mode. It doesn't fail technically. It waits. Each team reasonably wants to know the shape of the thing before committing to its part, and the shape isn't known until someone builds part of it. Ideas like this tend to die in the gap between "everyone likes it" and "who goes first".

02 · THE WRONG TURNWrite the spec, then wait in line

The conventional sequence is easy to picture. Write requirements. Design the CMS schema. Wait for content. Ask the hub team for an endpoint. Wait for it to reach dev. Hand the app team a payload. Wait for screens. Release. Every step is sensible, every step blocks on the one before, and the first time anyone sees the feature working is weeks after the idea, by which point changing its shape is expensive.

There's a quieter problem too. A written spec gets reviewed as prose, and prose hides the hard questions. Can a pick really join to a live collection record? Does the object know which room it's in? Can the app get from that room to a map? None of those come up in a document. They come up the first time you try to render the screen.

The fastest way to agree on a feature is to put a working one in front of people, on the day of the idea, and argue about that instead.


03 · THE KEY INSIGHTPrototype the contract, not the screens

The Content API already fronts the integration hub, the collections data, indoor wayfinding and the event feeds, and it serves everything in one envelope that says where each response came from. That meant a new feature didn't need new plumbing, only a new contract. On 24 September, starting from a pitch for a weekly curator's pick, we built it in an afternoon:

12:12 · Contract proposedEach pick has a hero, articles, up to ten collection objects with curator notes, and travel links. The editorial copy was labelled fixture data standing in for the CMS. Every object was a real record from the live collections data. A short contract document went to the app team and the CMS and hub owners.
CONTRACT
14:07 · The first real pickTen real objects from two exhibitions, written up by the person who pitched the idea. It ran from early-twentieth-century architectural terracotta to spacecraft.
REAL DATA
14:31 · Live wayfindingAn object knows its exhibition, and the exhibition knows its gallery and its indoor-map link, so every pick gets a working "Take me there".
JOIN
14:33 · In productionTested in Docker, merged through a pull request, deployed automatically.
SHIP
15:43 · Codegen-ready specThe OpenAPI spec pointed at production and modelled content blocks as a typed union, so the mobile app could generate its client instead of hand-writing one. The endpoints had already got their own section in the interactive docs.
HANDOFF

The same afternoon our showcase web app gained an app-style preview. It was a phone frame drawn in the visitor app's own design language, with a new Curated lane on the Discover screen between On Now and On Display, plus pick, story, object and travel screens. Every screen footer listed the API calls that built it. That preview was the pitch: stakeholders could tap through the experience, with real objects and real wayfinding, on the day the idea was raised.

Decision register · the model changed, the plumbing didn't

The prototype then went to the content and app leads for review, and the idea changed shape. A weekly curator's pick became staff picks: staff profiles told as Q&A, one set per venue, with no weekly publishing rhythm to keep up. That is exactly what a prototype is for. Changing the editorial model cost a conversation, not a rebuild, because the expensive question, whether a pick can join to real objects, events and wayfinding, had already been answered in production.


04 · THE ARCHITECTUREAuthor once, publish through the hub, read through a proxy

Here's the shape that shipped. Editors write in the CMS studio. The integration hub publishes picks per venue behind Azure API Management. The visitor app never calls upstream directly: its .NET backend proxies the hub and caches it, and the React Native app only talks to that backend, through a generated client. The Content API serves the same picks to web consumers, reading the hub first and the CMS directly when the hub has nothing to say.

HEADLESS CMS · SANITY pick document · one per venue Q&A → objects, events, shop, links OPERATOR TOOLING hub admin panel · inspect per venue gateway route map + API collections INTEGRATION HUB list picks · per venue get pick · per venue + slug AZURE APIM versioned API subscription key APP .NET BACKEND picks proxy · 5-minute cache CONTENT API picks per venue · source-labelled Visitor app · React Native generated client · React Query Web + other consumers showcase app · kiosks · partners publish hub first fallback · direct CMS read
The shipped shape. The CMS is the only place a pick is written. The hub is the published source behind the gateway. The app only reaches it through its own backend, which caches for five minutes in Redis and in memory. The Content API reads the hub first and falls back to the CMS directly, and every response says which one served it.

Who owned which layer

Each layer had one owner, and they met at one shape. Nobody waited on a document. They built against the contract and the stored model the hub already exposed.

LeadershipThe pitch, and the first real pick.
IDEA
CMS developerThe CMS studio and the pick document type, shaped to mirror the hub's stored model.
AUTHOR
Mobile app leadBackend proxy, DTOs, generated client, React Query hooks, the React Native screens, and the release.
APP
Solution architecture (me)The contract and prototype, the Content API endpoints, the hub admin panel, and the gateway route map with API collections.
PLATFORM
Content and data leadCMS, content and integration-hub direction, and review of the prototype.
REVIEW

05 · THE FLOWThirty hours, four lanes, one shape

Once the model was agreed, the build ran in parallel rather than in sequence. This is the record from the pull requests and the release pipeline, in local time, from the morning the CMS studio was created to the moment both store builds were uploaded.

CMS VISITOR APP HUB TOOLING CONTENT API RELEASE 20 OCT 09:00 18:00 21 OCT 00:00 06:00 12:00 18:00 studio schema pick documents data layer · proxy, client, hooks screens hub panel gateway routes picks endpoints · hub first, CMS fallback TestFlight 17:13 · Play internal 17:52
Parallel, not sequential. The app's data layer merged less than an hour after the CMS schema, and a day before the screens. Hub tooling and the Content API landed the next morning. The release pipeline took twelve minutes to reach TestFlight and fifty-one to reach the Play internal track. Every dot is a reviewed, merged pull request.

The app's data-layer pull request is the one I would point to. It merged twenty-three minutes after it was opened, on the same afternoon the CMS schema landed. That only works when the payload shape isn't in dispute. The backend endpoints, DTOs, regenerated OpenAPI spec, generated client and React Query hooks were all written against a model that already existed in the hub. The screens followed the next afternoon, with simulator screenshots in the pull request and every required check green.

Thirty hours was not anyone typing fast. The hard questions were settled in September, so in October four teams could build side by side instead of in a queue.


06 · THE DETAIL THAT SEPARATES REAL FROM DEMOEvery response says where it came from

A prototype built on fixture copy is only safe if nobody can mistake the fixture for the real thing. Every Content API response carries a meta.source. The September prototype said curator-demo on every editorial field. The October endpoints say dxp-hub when the hub answered and picks-sanity when they read the CMS directly. A consumer, a tester or a support analyst can tell what they're looking at without reading code.

That labelling earned its keep within a day. When the Content API endpoints were first wired up, the development hub returned an empty list for every venue. The CMS's development dataset held six published profiles and its production dataset held none, so the development hub was reading the empty one. The staging hub, meanwhile, already served all six.

Decision register · fall back, don't block

Rather than wait for the development hub to be repointed, the Content API reads the hub first and falls back to the CMS's public CDN when the hub is unset, unreachable, unauthorised or empty, and labels the answer accordingly. Development carried on against real authored content, the upstream fix was logged, and nothing pretended to be the hub when it wasn't. Once the hub answers, the label flips back to dxp-hub by itself, with no release needed.

The app takes a stricter line on purpose. It never reads the CMS directly. It reads the hub through its own backend with a five-minute cache, so an editor's change reaches phones within minutes and a hub outage never turns into a stampede of retries from every device in the building.

07 · THE PIPELINENobody got a special deploy path

The speed came from the shape of the work, not from skipping anything. Every repository kept its own branch protection, review and checks, and the mobile app used the same release pipeline as every other feature.

Pull request checksUI build, unit and integration tests, Docker build, release-config build, a secret scan and a preview deploy. A feature can't merge red, or with a leaked secret.
GATE
Release gate · version bumpA merge only produces a release when the app version goes up, so a release is a decision, not a side effect.
INTENT
Secrets validationSigning certificates, store API keys, the Android keystore and the crash-reporting token are checked before forty minutes of build time is spent.
FAIL FAST
iOS · fastlane → TestFlightBuilt and uploaded in twelve minutes.
APPLE
Android · fastlane → Play internal trackThe track is hard-coded behind a safety check, so promotion to production stays a human decision.
GOOGLE
Production · release branch + version tagThe public store release is a separate, tagged build with production configuration, promoted when we're ready.
STORES

The Content API follows the same discipline at its own scale. Work lands on a test branch, tests and type checks run in Docker, a pull request goes to main, and CI runs on a self-hosted runner to keep cost down. An endpoint isn't finished until it is in the OpenAPI spec and the interactive docs.


08 · THE REPEATABLE PARTThe workflow for the next feature

This is the first feature to travel the whole path end to end. It shouldn't be the last, so here is the pattern written down as steps another team can follow without me in the room.

09 · THE PROOFWhat shipped, and what comes next

This is what the commits, pull requests and pipeline runs showed on 21 October:

The rest of the road is deliberate, not unknown:

What matters most to me about that list is that it is all content, configuration and promotion. None of it is architecture, and none of it needs another team to start before it can finish.

← All writing rummanahmed.com Share on LinkedIn