Architecture Overview

How Certivant structures its pnpm Turborepo monorepo with Next.js for the platform and Vite for browser-only tools, including the iframe postMessage communication pattern.

Certivant uses two build tools — Next.js and Vite — in a single pnpm + Turborepo monorepo. They do not overlap; each owns the surfaces it is best suited for.

Monorepo structure

apps/
  web/           ← Next.js  — platform, dashboard, admin, docs
  email-editor/  ← Vite     — GrapesJS email template editor

packages/        ← shared libraries (UI, API, auth, database …)
tooling/         ← shared config (TypeScript, ESLint, Biome …)

Where Next.js runs

Next.js is the backbone of the platform. It handles every surface that benefits from SSR, routing, server components, auth middleware, or SEO:

SurfaceWhy Next.js
Marketing siteSEO, ISR, image optimisation
Customer dashboardAuth middleware, server components
Admin consoleServer actions, role-based access
Hosted verification pagesSSR + middleware for session checks
Documentation siteStatic generation via Content Collections

Where Vite runs

Vite powers self-contained tools and embeddable modules that need fast iteration, isolation, or browser-native APIs that conflict with SSR:

SurfaceWhy Vite
Email template editorGrapesJS + Monaco — browser-only, no SSR
Verification widgetEmbeddable certivant.js snippet
SDK bundlesLibrary mode, tree-shakeable
Flow builder (planned)React Flow + Monaco
Liveness capture (planned)WebRTC + WASM, camera APIs

Communication pattern

Vite apps that live inside a Next.js page (e.g. the email editor) are mounted in an <iframe> and communicate via postMessage. This gives complete isolation — the Vite app runs in its own origin, with its own CSS pipeline and Web Workers, without fighting Next.js/Turbopack.

Next.js page
  └── <iframe src="http://localhost:5173">   ← Vite app
        ↕ postMessage
      { type: "INIT"   } parent → iframe     initial data
      { type: "READY"  } iframe → parent     editor loaded
      { type: "CHANGE" } iframe → parent     canvas mutation
      { type: "SAVE"   } iframe → parent     user confirmed edit

Development

One command starts all apps:

pnpm dev
# ├── @repo/web          → http://localhost:3005  (Next.js)
# └── @repo/email-editor → http://localhost:5173  (Vite)

Turborepo runs them concurrently. Each app has its own HMR — changes to the email editor reflect instantly inside the iframe without a Next.js rebuild.

Production

Vite apps compile to pure static files (dist/). There is no additional server process at runtime.

pnpm build
  ├── @repo/email-editor → apps/email-editor/dist/  (deploy to CDN)
  └── @repo/web          → apps/web/.next/           (deploy to VPS/Vercel)

Turborepo ensures @repo/email-editor always builds before @repo/web (configured in turbo.json via @repo/web#build).

DevelopmentProduction
Next.js✅ running (port 3005)✅ Node.js server
Vite dev server✅ running (port 5173)❌ not needed
Vite built files—✅ static files on CDN
Extra server costnonenone