Architecture

Scaling a Multi-Platform App Suite with Turborepo

How XAppNova keeps a small portfolio of apps, extensions, and marketing surfaces aligned without turning maintenance into a full-time job.

The small-app paradox

Small apps are supposed to stay simple, but a portfolio of small apps becomes operationally messy very quickly. Every product needs landing pages, screenshots, legal pages, support copy, analytics instrumentation, and release housekeeping. If each app lives in its own disconnected setup, the overhead starts to outweigh the product itself.

That is the problem we wanted to avoid at XAppNova. We build focused tools across different surfaces: Safari extensions, Chrome extensions, iPhone apps, macOS utilities, and a marketing site that explains all of them. The apps are intentionally small. The system around them cannot be chaotic.

The temptation with a growing portfolio is to copy the working setup from one product and adapt it for the next. That produces consistent results initially, but it means every shared piece — design tokens, analytics events, content types, validation logic — has N copies that will diverge over time. The fifteenth time you fix a spacing issue in a component, you realize you have fourteen other versions of the same component that also have the problem.

Why we chose a monorepo

A monorepo does not mean a single application. It means keeping multiple packages and applications in one repository with shared tooling, shared version control, and a build system that understands the dependency graph between them.

We use Turborepo to manage the build graph. The goal is not scale for its own sake — a portfolio of five apps is not large by enterprise standards. The goal is to remove the repeated work that accumulates around a product portfolio: duplicated type definitions, design token drift, analytics wiring that diverges between apps, validation scripts rewritten from scratch for each product.

The monorepo gives us one predictable place for each shared concern. Design tokens live in one package. App metadata lives in one package. Analytics event definitions live in one package. Shared ESLint and TypeScript configurations live in one package. When any of those change, they change once.

  • packages/ui — design tokens (CSS custom properties), shared UI components
  • packages/content — TypeScript schemas for app and category data, JSON files, validation script
  • packages/analytics — event type definitions, tracker implementation, UTM helpers
  • packages/config — shared ESLint and TypeScript configurations
  • apps/web — Next.js 15 marketing site that consumes all of the above

Shared design tokens in practice

Design tokens are the clearest example of what shared packages prevent. Without them, colors, spacing values, border radii, and typography scales get copied into each project and immediately start diverging. A spacing value that gets updated in the marketing site stays different in the support pages. A brand color that shifts slightly gets updated in some components and not others.

In the packages/ui package, design tokens are defined as CSS custom properties in a single index.css file. The marketing site imports that stylesheet at the root layout level. Every component in the app, regardless of which page it lives on, reads from the same token set. When a brand color changes, it changes once.

The same package contains shared UI components — app cards, store buttons, screenshot carousels — that are reused across different pages. Because they are in a separate package, the web app imports them exactly like any other npm package, with full TypeScript types and tree-shaking. The build only includes what is actually used.

How the build cache reduces CI cost

Turborepo tracks task inputs and outputs using a content-addressed cache. When you run the build, Turborepo checks whether the inputs to each task have changed since the last run. If the packages/ui source files have not changed, the packages/ui build task is restored from cache rather than re-executed.

In practice, this means that most CI runs only rebuild the packages that were actually affected by the change. A pull request that only updates an app's JSON content file rebuilds the web app but not the shared packages that did not change. A pull request that updates a shared component rebuilds every package that depends on it.

For a small team running on a tight CI budget, this makes a real difference. A full cold build that would otherwise take several minutes completes in under a minute when most of the graph is cached. The feedback cycle on pull requests is fast enough to stay in flow while waiting for CI.

What this changes day to day

The practical benefit is not abstract architecture purity. It is speed with fewer mistakes. Adding a new app to the portfolio is a content task — create a JSON file, add the images, run the validation script — not a mini-site rebuild. Updating a store URL or app tagline happens once and then flows to every page that renders it.

This also makes product quality easier to defend. When the website, legal pages, and app detail pages all read from the same content model, inconsistency between them becomes visible immediately. The validation script that runs before every deployment catches structural errors in the data before they reach the live site.

The cross-cutting benefit that is hardest to quantify is reduced context-switching. When everything is in one repository, a change that touches the content schema, the web component that renders it, and the analytics event it emits lives in one pull request with one review. There are no coordination steps between repositories, no version pinning to update manually, no wondering whether the consumer of a changed API has been updated.

Adding a new app to the portfolio

The practical test of any content system is how much effort the next addition requires. For the XAppNova portfolio, adding a new app requires: creating a JSON file in packages/content/data/apps/ that conforms to the App TypeScript interface, adding image assets to the public images directory, running the validation script, and pushing the change.

The marketing site picks up the new app automatically. The app directory page adds the new entry. The sitemap includes the new URLs. The category pages include the app if its categories match. The home page featured grid includes it if a featuredRank is set. All of that happens from the content of the single JSON file.

That is the payoff of the structured content approach working together with the shared package architecture. A well-defined content model, validated before publication, consumed by a static site that regenerates at each build, means that the scope of a new product addition is bounded. You add the data, the site does the rest.

The real payoff

For a small team, consistency is leverage. A shared system lets us spend more time on the actual product behavior and less time fixing the same issue in three places, updating the same value in six files, or wondering whether the version of a component on one page matches the version on another.

That is the core reason the stack matters. We are not trying to look like a large software company with enterprise tooling. We are trying to keep a compact app suite maintainable as it grows without lowering the quality bar or adding headcount to absorb the overhead.

Turborepo is not magic — it is a build orchestrator with a cache. But combined with a disciplined package structure and typed content, it gives a small team the kind of operational leverage that would otherwise require significantly more coordination overhead to achieve.