All articles
14 · Engineering · 2026.08 · 5 min

One product, three surfaces: web, service and mobile in one monorepo

Motice is a CRM/ERP product for pilates, yoga, personal training and wellness studios. The administration app, customer portal and mobile app are different faces of the same business processes. This article is about what it means to keep them in one TypeScript monorepo.

01The cost of scattered tools

Daily work in an appointment-based studio moves between bookings, customers, classes and business operations. When these are spread across different tools, both the business and the customer look for the same information in several places. Motice brings these flows into a shared product that businesses manage within their own separate workspaces.

The cost of this kind of scattering rarely shows up as one big failure; it builds up as small frictions. A piece of information is updated in one place and stays stale in another, and the same question gets two different answers from two different sources. In appointment-based work, time itself is part of the product, so inconsistencies like these land directly on the business's day. In my view, this is exactly where the value of gathering the flows into one product model lies.

02Three surfaces, one model

The product has three surfaces: the administration app the business uses, the portal customers reach from the browser, and the customer app built with Expo. All three look at the same booking, the same customer, the same class. So we treat them not as separate projects but as different views of a shared product model.

Each surface has different users and expectations. The business side wants a wide view, quick editing and the ability to see operations at a glance. The customer wants a short path to their own booking and their own information, sometimes from a browser, sometimes from a phone. Screens can differ, but the underlying concepts must stay the same. What a booking is and whom it belongs to should get the same answer whichever surface you look from.

structure
TypeScript monorepo
├─ React + Vite   web interfaces
├─ Expo           customer app
├─ Fastify + tRPC service layer
└─ PostgreSQL + Drizzle ORM

03Why a monorepo

A single TypeScript monorepo keeps the React interfaces, the Fastify and tRPC service layer, and the PostgreSQL and Drizzle data layer under one roof. Shared components aim for consistency between web and mobile, and with tRPC the interfaces speak the same types as the services. When a field's name or shape changes, the change becomes visible at compile time on every surface involved.

This has a price too. A monorepo makes it very easy to loosen boundaries; since everything is in one place, it becomes easy for one package to reach into another's internals. That is why I think structures like this require choosing deliberately what to share: types, contracts and genuinely common components should be shared, while behavior specific to one surface should stay there. The point of shared components is not to make everything identical, but to make the same concept look the same way everywhere.

Software that understands a business's daily rhythm is measured by how its flows fit together, not by a single screen.

04Workspaces and data boundaries

In Motice, businesses work within their own separate workspaces. In products like this, one of the most important questions for me is that one business's data never mixes into another business's view under any condition. Trying to guarantee that with a filter in the interface alone is fragile; thinking of the boundary in the service and data layers, as part of every request's context, feels much sturdier to me. A typed service and data layer helps make boundaries like this visible in code.

The same thinking applies to the calendar. Classes, bookings and customers may look like independent records, but in reality they constantly refer to each other. When a change is made in one place, the other surfaces reflecting it in the same way is the basic condition for users trusting the product.

In products like this, the hard part is usually not individual features but the places where features intersect. A class, the bookings for that class and the business's view of the day are really different readings of the same information. Expressing these intersections in a shared model keeps every new screen from inventing its own rules from scratch.

05What I find most interesting

Motice is a shared product of the Unitic team, and I contribute to its development within the team. What draws me most in this project is not finishing a single screen, but making calendar, customer and operations flows complement each other correctly inside the same product. One benefit of working as a team on a shared codebase is seeing early how a change affects someone else's work. Shared types make that conversation concrete: when a contract changes, the affected places show up without needing a debate.

Working on a product that is still in development is its own experience. Because decisions are not frozen yet, the model built today needs to be able to carry flows added tomorrow. So when I work on a structure, I try to think not only about today's screen but also about how that structure might grow later.

Related articles
  1. 15Blockchain on real devices: 72 hours with three Raspberry PisR&D and hardware2026.075 min
  2. 16Before writing code: clarifying decisions in MentorualProduct & analysis2026.065 min