Skip to main content
Kinan Khoja

Public Portfolio Project — Open Source

Multi-Tenant Frontend Monorepo

Overview

An open-source, public architecture demo — a Turborepo and pnpm monorepo showing how two independently deployable tenant apps, Nova and Aurora, share the same package graph (UI, theme, query, API, and feature packages) while running tenant-specific configuration and mock data.

Context

Built as a standalone technical demonstration — not affiliated with any employer or client codebase — to show, end-to-end, how a multi-tenant frontend platform can be architected: shared UI, theme, query, API, and feature packages consumed by two independently deployable tenant apps, each with its own configuration and tenant-aware mock data, all inspectable in a public repository.

Contribution

  • Designed and built the full monorepo architecture from scratch using Turborepo and pnpm workspaces
  • Built two independently deployable tenant apps, Nova (web-client) and Aurora (tenant-client), sharing the same package graph
  • Built eight shared top-level packages spanning UI, theming, configuration, API access, query, and mock data
  • Built shared auth-demo and account-demo feature packages consumed with identical implementation by both tenant apps
  • Built a tenant-aware mock runtime with stateful session, profile, preferences, and account data — no backend required
  • Committed a pnpm-lock.yaml so installs are deterministic and reproducible across the workspace

Technology

  • React
  • TypeScript
  • Vite
  • Turborepo
  • pnpm Workspaces
  • TanStack Query
  • Zustand
  • Vercel

Confidentiality

Public Portfolio Project — Open Source

Engineering Case Study

Monorepo Architecture

The project is structured as a Turborepo and pnpm workspace monorepo: two independently deployable apps — web-client (Nova) and tenant-client (Aurora) — consume a shared set of internal packages rather than duplicating logic across tenants.

Turborepo's task graph and caching keep builds, linting, and type-checking fast as the workspace grows, while pnpm workspaces keep dependency linking explicit — and a committed pnpm-lock.yaml keeps installs deterministic across machines and CI.

Shared Package Boundaries

Eight top-level packages — ui, theme, config, api, query, mock-api, eslint-config, and typescript-config — form the shared foundation both apps build on, alongside two shared feature packages under packages/features.

Keeping these concerns in separate packages with clear boundaries makes the split between shared platform code and tenant-specific configuration explicit, rather than something that has to be inferred from application code.

Nova & Aurora: Shared Implementation, Tenant-Specific Configuration

Nova (web-client) and Aurora (tenant-client) are two independently deployed apps that share the exact same package graph — UI, theme, query, API, and feature packages — while running distinct branding, configuration, and mock account data.

The two tenants also demonstrate that shared implementation doesn't mean identical runtime data: Nova's mock account carries a $1,250 balance with +300 / -150 / +500 activity under the profile "Nova Demo Account," while Aurora's carries a $2,840 balance with +650 / -220 / +180 activity under "Aurora Demo Account" — same code path, tenant-specific state.

Nova tenant home screen, showing the shared Turborepo architecture rendered with Nova's own theme and branding.
Shared architecture, same page structure — Nova's own theme and branding applied on top.
Aurora tenant home screen, running the same shared packages and page structure as Nova with a different theme and branding.
The same shared architecture, packages, and page structure — a deliberately different theme and tenant configuration.

Shared Auth + Account Feature Packages

Authentication and account management live in packages/features/auth-demo and packages/features/account-demo, mounted with identical implementation in both tenant apps rather than reimplemented per app.

Both packages run against real TanStack Query hooks into the mock runtime: two-step login, protected routing, profile, preferences, and account activity all behave like a backend-driven feature, not a static UI mockup. Session state in auth-demo is held in Zustand; the rest of the shared packages compose React context and TanStack Query.

Nova and Aurora render this same component tree from different tenant-aware mock fixtures: Nova shows a Standard membership, a $1,250 balance, and mobile number 555-0100 under "Nova Demo Account"; Aurora shows a Preferred membership, a $2,840 balance, and 555-0142 under "Aurora Demo Account." The implementation isn't duplicated — only the fixture data behind it differs per tenant.

Nova's account screen from the shared account-demo feature package: Nova Demo Account, Standard membership, $1,250 balance, mobile number 555-0100.
Nova — Standard membership, $1,250 balance, rendered by the shared account-demo package.
Aurora's account screen from the same shared account-demo feature package: Aurora Demo Account, Preferred membership, $2,840 balance, mobile number 555-0142.
Aurora — Preferred membership, $2,840 balance, the identical package rendering different tenant fixture data.

Tenant-Aware Mock Runtime

packages/mock-api stands in for a real backend: a route-matching dispatcher intercepts requests from both apps and returns tenant-specific session, profile, preferences, and account data, so the whole platform runs and is demoable without any external service.

The mock layer holds real in-memory state per tenant, so mutations — updating a profile, toggling a preference — persist for the session the way a backend-driven application would, and Nova and Aurora never see each other's state.

Query + API Abstraction

packages/api wraps native fetch in a minimal client — no Axios or other HTTP library — that both tenant apps and the shared feature packages call through.

packages/query builds typed useQueryApi / useMutationApi hooks on top of TanStack Query, so data fetching, caching, and mutation state are handled consistently across every package that needs them.

Architectural Decisions & Tradeoffs

Splitting functionality into eight top-level packages plus two feature packages adds coordination overhead: dependency management, package boundaries, and build orchestration all get more complex than a single app would require. That overhead is accepted deliberately, because reuse across genuinely independent tenant apps — not reuse in name only — is the point the case study exists to demonstrate.

Turborepo's caching and task graph, pnpm's strict workspace linking, and a committed lockfile are what make that granularity practical instead of just theoretically clean — without them, the coordination cost would outweigh the reuse benefit.

← Back to Projects