The trap everyone walks into
App-builder platforms usually start the same way: take an existing app, extract the colours and the logo into a config file, call it a template. That gets you a demo in two weeks and a dead end in six months, because the moment a customer wants a different screen — not a different colour — you’re forking the codebase. Fork once and you have a template. Fork fifty times and you have fifty apps to maintain and no platform.
The thesis here was that the platform’s real product is the config plane, and the apps are just what falls out of it. That inverts the work: instead of building apps and extracting configuration, you design a configuration language expressive enough that apps are a rendering of it.
Constraints that shaped the design
The audience included non-engineers. Developers, designers, and people who had never opened a terminal all had to be able to assemble something shippable. That rules out anything that leaks build semantics upward.
Native means app-store review. A PWA can change on deploy. An iOS binary cannot. So the architecture had to be honest about which changes are config (shippable over the air, instantly) and which are capability (require a new binary and a review cycle). Blurring that line is how these platforms end up promising instant changes and delivering week-long waits.
A legacy estate was already in production. PHP on Symfony and Yii, Python on Flask, MySQL, MongoDB and Elasticsearch — all live, all carrying revenue. A rewrite was never on the table. The platform had to be additive.
Decisions and trade-offs
A capability manifest, not a theme file
Every generated app is described by a manifest: which screens exist, what data each binds to, which service integrations are enabled, what the navigation graph looks like, and how it’s skinned. The app shell is an interpreter for that manifest rather than a fork of a reference app.
The trade-off is that the manifest becomes a language, and languages need versioning, validation and migration paths. I put a compiler in front of it — validate, resolve references, emit — precisely so that a bad configuration fails at build time with a legible error instead of at runtime on a customer’s phone.
Draw the config/capability line early and enforce it in the type system
Anything the shell can interpret without new native code is config: content, copy, layout within known screen types, theming, feature flags, endpoint bindings. Anything that needs new native surface area is capability, and capability changes flow through the build pipeline and the store.
Making this a structural property of the manifest — rather than a convention in a wiki — meant the configurator could show an author which of their changes ship instantly and which need a release. That single piece of honesty removed most of the support load I’d expected.
Headless CMS decoupled from the data-config layer
It’s tempting to make one system that holds both “what the app says” and “what the app is”. They have completely different change rates and completely different blast radii. Content changes hourly and affects one screen. Structure changes rarely and can break every generated app.
Keeping them separate cost an extra integration seam. It bought the ability to let a marketing user edit content freely without any possibility of structurally breaking an app in the field.
Granular ACL as infrastructure, not a feature
The customer shape was agencies and multi-brand organisations: one org, many apps, many collaborators, and a genuine need to say “this person can edit copy on these two apps but cannot touch integrations anywhere.” Retro-fitting that later would have meant re-auditing every write path.
So permissions were resolved at the config plane — org, app, and field scope — and every mutation route went through the same check. It’s unglamorous work that nobody demos, and skipping it is how platforms end up with an admin role that is secretly root.
Introducing Node and React alongside PHP, not instead of it
New platform services were built on Node.js and React/React Native while the legacy Symfony/Yii/Flask estate kept running. Shared concerns moved behind service boundaries first, which is what made an incremental path toward microservices possible at all. The unglamorous truth is that most of the value came from agreeing the seams, not from the new runtime.
Governance, because architecture is a team sport
Alongside the platform I was one of three architects protecting code quality across the wider engineering estate. Every critical change got a written solution document — the problem, the options considered, the decision, the cost and time assessment, and what we’d give up. Writing the trade-off down before building is the cheapest debugging tool that exists, and it’s the one most teams skip.
What I’d do differently in 2026
I’d generate the configurator UI from the manifest schema rather than maintaining them in parallel — the drift between “what the schema allows” and “what the UI exposes” was the most persistent source of small bugs.
I’d also lean much harder on the PWA target. In 2019 the native binary was the credible artifact and the PWA was the compromise. That ordering has largely reversed for the class of apps this platform generated, and building PWA-first would have collapsed the slowest part of the whole pipeline.