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:
| Surface | Why Next.js |
|---|---|
| Marketing site | SEO, ISR, image optimisation |
| Customer dashboard | Auth middleware, server components |
| Admin console | Server actions, role-based access |
| Hosted verification pages | SSR + middleware for session checks |
| Documentation site | Static 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:
| Surface | Why Vite |
|---|---|
| Email template editor | GrapesJS + Monaco — browser-only, no SSR |
| Verification widget | Embeddable certivant.js snippet |
| SDK bundles | Library 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).
| Development | Production | |
|---|---|---|
| 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 cost | none | none |