Project Structure
Understanding the monorepo structure and organization.
Learn how the MVS Telecom Operator Console codebase is organized and where to find things.
Monorepo Overview
This project uses Turborepo to manage a monorepo with multiple apps and packages.
project-root/ ├── apps/ # Applications │ ├── web/ # Main Next.js operator console │ └── e2e/ # Playwright E2E tests ├── packages/ # Shared packages │ ├── ui/ # UI components │ ├── database/ # Prisma schema, client & migrations │ ├── billing-usage/ # Pure billing/usage rating engine │ ├── provisioning-workflows/ # Saga FSM, services, crons │ ├── vendor-core/ # Carrier registry + neutral adapter │ ├── vendor-twilio/ # Twilio carrier adapter │ ├── vendor-voipms/ # VoIP.ms carrier adapter │ ├── vendor-yeastar/ # Yeastar/YCM Cloud PBX adapter │ └── vendor-yealink/ # Yealink YMCS + RPS adapter ├── tooling/ # Development tools └── docs/ # Repo/contributor documentation
Main Application (apps/web)
The primary Next.js 16 (App Router + RSC) application:
apps/web/ ├── app/ # Next.js App Router │ └── [locale]/ # Locale-prefixed routes (see below) ├── content/ # In-app product docs & help content ├── config/ # Configuration files ├── lib/ # Utility functions (incl. lib/digest, lib/copilot) └── public/ # Static assets
Route Structure
All routes live under app/[locale]/, organized into route groups:
app/[locale]/ ├── (internal)/ # Authenticated operator console (auth-gated) ├── (public)/ # Public marketing pages ├── (invitation)/ # Invitation acceptance flow ├── auth/ # Sign-in / sign-up / password reset ├── admin/ # Super-admin pages └── help/ # In-app documentation / help
The (internal)/ layout enforces auth (redirects to sign-in if no session) and handles organization selection. All new authenticated pages go here.
Internal Console Routes ((internal))
The authenticated operator console renders inside the navy "Telecom Console" shell (fixed navy rail + frosted topbar + ⌘K palette; _config/nav-items.ts is the single source for both the sidebar and the palette):
app/[locale]/(internal)/ ├── dashboard/ # Mission Control overview + AI MorningBrief card ├── customers/ # Customer list; [id] = Customer 360 ├── tenants/[id]/ # Tenant detail (CDR / recordings / config tabs) ├── onboarding/ # Onboarding saga FSM pipeline; [jobId] = detail ├── tasks/ # Human-task queue + port-doc upload; [taskId] ├── billing/ # Operator billing: plans, MRR, invoices, KPIs ├── operations/ # Read-only exceptions + health KPI console ├── audit/ # Append-only audit-event log (filterable) ├── carriers/ # Carrier cards + org routing policies ├── numbers/ # Phone-number inventory + buy / port-in ├── inventory/ # DID + device pool ├── devices/ # Device fleet (RPS, firmware, keypad lock, online) ├── sync/ # Config-sync / drift └── settings/ # Org / account settings
Auth Routes (auth)
app/[locale]/auth/ ├── sign-in/ ├── sign-up/ ├── password-reset/ └── verify/
Packages Structure
Vendor & Carrier Packages
The carrier abstraction lives in vendor-core, with one package per vendor:
packages/ ├── vendor-core/ # CarrierRegistry + neutral CarrierAdapter interface ├── vendor-twilio/ # Twilio: DID, toll-free, port-in, E911, SMS ├── vendor-voipms/ # VoIP.ms: advertises 'did' only (port-in/E911 deferred) ├── vendor-yeastar/ # Yeastar/YCM Cloud PBX (PCE) provisioning + passthrough └── vendor-yealink/ # Yealink YMCS (config/BLF/firmware) + RPS zero-touch
buildCarrierRegistry() registers Twilio + VoIP.ms only — there is no Sinch adapter in code. Routing applies only to switchable number ops; MVS's own Twilio SIP-trunk / IP-ACL / PBX plumbing stays bound to the Twilio dep.
Provisioning Workflows (@kit/provisioning-workflows)
The orchestration + I/O layer:
packages/provisioning-workflows/ ├── functions/ # Inngest functions, saga stages, crons, lib/deps.ts └── services/ # billing.service, carrier-routing.service, etc.
Crons (registered in functions/index.ts, mounted at /api/inngest): device-checkin, provisioning-resync, reconcile-ports, provisioning-config-sync, collect-usage (did_count meter only). Saga stages and job-resume are event-driven, not crons.
Billing & Usage (@kit/billing-usage)
Pure rating engine — no I/O:
packages/billing-usage/
└── src/
├── meters.ts # BillingMeter discriminated union (4 kinds)
└── pricing.ts # rateUsage, projectCost, MILLICENTS_PER_CENT
Money units are load-bearing: per-unit rates are integer millicents (1¢ = 1000 millicents); money (base/line/invoice amounts, MRR) is integer cents. They are never mixed; per-line rounding is half-up.
UI Package (@kit/ui)
Shared UI components (Base UI + Tailwind 4 + Lucide):
packages/ui/
└── src/
├── components/ # UI components
└── ...
Database Package (@kit/database)
Prisma 7 schema, client, and migrations (38 models, 18 migrations):
packages/database/ ├── prisma/ │ ├── schema.prisma # 38 models (auth, provisioning, carrier, billing) │ └── migrations/ # 18 migrations (applied to prod Neon manually) └── src/ # Database client
Migrations are applied to prod Neon manually — Vercel does not auto-run prisma migrate deploy. Status-like columns are CHECK-constrained Strings, not native enums, across the provisioning / carrier / billing domains.
Configuration Files
Root Level
project-root/ ├── package.json # Root package.json ├── turbo.json # Turborepo config ├── pnpm-workspace.yaml # PNPM workspace └── tsconfig.json # Base TypeScript config
Application Level
apps/web/ ├── next.config.js # Next.js configuration ├── tsconfig.json # TypeScript config └── .env.local # Environment variables
Naming Conventions
Files
- Pages:
page.tsx(Next.js convention) - Layouts:
layout.tsx - Loaders:
{feature}-page.loader.ts - Server Actions:
{feature}-server-actions.ts - Schemas:
{feature}.schema.ts - Components / utilities:
kebab-case.tsx/kebab-case.ts
Directories
- Route segments:
[param]for dynamic - Route groups:
(group)for organization - Private folders:
_components,_lib,_config - Parallel routes:
@folder
Code Organization
Each internal feature collocates its page, loader, actions, schema, and private folders:
(internal)/billing/ ├── page.tsx # Route page ├── billing-page.loader.ts # RSC data loader ├── billing-server-actions.ts # next-safe-action server actions ├── billing.schema.ts # Validation schema ├── _components/ # Private components └── _lib/ # Private utilities
Import Paths
Use TypeScript path aliases:
// Absolute imports from packages
import { Button } from '@kit/ui/button';
import { rateUsage } from '@kit/billing-usage';
// Relative imports within a feature
import { loadBilling } from './billing-page.loader';
import { CustomerBillingTable } from './_components/customer-billing-table';
Best Practices
- Keep route-specific code private - Use
_components,_lib,_config - Share reusable code - Extract to packages
- Collocate related files - Page, loader, actions, and schema live together
- Use consistent naming -
{feature}-page.loader.ts,{feature}-server-actions.ts - Organize by feature - Not by file type
Finding Your Way
| Looking for... | Location |
|---|---|
| UI Components | packages/ui/src/components/ |
| Prisma Schema | packages/database/prisma/schema.prisma |
| Migrations | packages/database/prisma/migrations/ |
| API Routes | apps/web/app/api/ |
| Internal Console Pages | apps/web/app/[locale]/(internal)/{feature}/ |
| Page Loaders | {feature}-page.loader.ts |
| Server Actions | {feature}-server-actions.ts |
| Carrier Adapters | packages/vendor-*/ |
| Billing Engine | packages/billing-usage/src/ |
| Provisioning Services | packages/provisioning-workflows/services/ |
| Sidebar / ⌘K Nav | apps/web/app/[locale]/(internal)/_config/nav-items.ts |