B2B SaaS Platform

B2B SaaS Platform

Next.js 13TurborepoTanstack QueryChakra UI

Context

A B2B SaaS platform built from scratch for the automotive industry:

  • 🖥️ A specialized CMS enabling client organizations (dealerships, resellers, leasing companies) to create, manage, and publish their vehicles and transactions.
  • 🤝 A CRM to manage their business operations.

Vehicles are polymorphic business objects, consumed end-to-end by an ecosystem of interconnected applications, each displaying and manipulating them within its own context: management back office, dynamically generated public website, internal ERP, customer portal, etc.

I worked across all of these applications for 3 years, including several months as the sole frontend developer after the project launch.

Polymorphic modeling

The core of the project relied on a discriminated and nested data model. Every resource created through the CMS is not a generic object, but a typed one whose structure varies depending on its discriminator.

Polymorphic data model

A flattened model forces the UI to handle optional properties (everything exists, nothing is guaranteed). A polymorphic model, discriminated by type, allows each object to expose only relevant properties, enabling strict typing, reliable validation, and precise rendering without unnecessary conditions.

Here is a simplified representation of the model:

  • A vehicle (car · motorcycle · truck · etc.) shares a common base, with type-specific attributes.
  • A vehicle includes commercial conditions (sale · leasing · rental), which are also polymorphic.
Example of business case matrix

This model produces a matrix of business cases that must be explicitly implemented end-to-end (creation/update forms, validation, rendering across each application). Each combination is a distinct case. This level of exhaustiveness is what allows the UI to accurately reflect every nuance of the domain.

Frontend implementation

Translating a discriminated model into a UI is not just about if/else conditions. The real architectural question was: where should the discriminating logic live, and how can it be encapsulated so that it remains maintainable as the number of business case combinations grows?

Complexity isolation & composition

The goal was to keep discriminating logic as close as possible to where it is needed, avoiding unnecessary propagation of complexity up the component tree.

Dynamic assembly of a specific component

Each vehicle or transaction type has its own components, dynamically assembled based on the discriminator.

In some cases (such as the vehicle detail page or the characteristics section in the back-office form), discrimination happened at the page level, determining which view to render based on the type. Each view then combined shared and specific sections through composition.

Strict type safety

Discriminated unions in TypeScript ensured that every business case was handled exhaustively. The compiler flagged any unhandled combinations.

Contextual validation

Validation rules varied depending on the type. A leasing form does not validate the same fields as a sales form (and may follow different business rules). Errors were handled on a per-case basis.

Monorepo structure

A shared codebase (monorepo) ensured visual and behavioral consistency across applications: presentational UI components, design system, and tooling. Each application had its own data access layer (dedicated BFF), with DTO types automatically generated from Swagger documentation using the Orval library. This eliminated manual duplication while maintaining a clear separation of concerns.

Methodology & team culture

Small team (2 frontend developers, 2 backend developers). Systematic code reviews, agile rituals (standups focused on unblocking rather than reporting, sprint planning, sprint retrospectives), and regular stakeholder demos.

Takeaways

3 years working on a full-scale SaaS platform, built from scratch up to version 2.6. This project taught me to:

  • 🧬 Maintain consistency of a complex model across multiple applications
  • 🤝 Collaborate within a team with a strong quality culture (reviews, ownership, demos)
  • 🕵️ Anticipate architectural decisions rather than react to them
  • 🗄️ Gain exposure to backend challenges and data modeling, which now drives my evolution toward full-stack