Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
TorbatYar Frontend — Migration Plan
Date: 2026-07-26
Status: Planning document — no code changes applied
Prerequisite: Read frontend-architecture-audit.md
This plan prioritizes stabilization before restructuring. Each phase is independently shippable.
Guiding Principles
- Do not refactor working modules mid-feature — changes are incremental and gated
- Enforce boundaries with tooling, not convention alone
- Accounting follows beauty/healthcare route patterns over time, not immediately
- Shared abstractions emerge from deduplication, not upfront framework design
- Keep the monolith until build times or team size force extraction
Phase 0 — Stabilize (Week 1)
Goal: Green build, remove dead weight, zero functional changes.
| Task | Files | Effort | Risk |
|---|---|---|---|
Fix DS Button type error OR remove loading prop usage |
app/accounting/purchase/returns/page.tsx, possibly components/ds/Button.tsx |
S | Low |
| Remove unused files (5 confirmed dead) | lib/api-client.ts, lib/optional-sso.ts, lib/beauty-business-nav.ts, components/HealthStatus.tsx, components/beauty/pages/mobile.tsx |
S | Low |
| Remove or use unused npm deps | @tanstack/react-table, recharts from package.json |
S | Low |
Add npm run typecheck script |
package.json |
S | None |
Verify npm run build passes in CI |
CI config | S | None |
Exit criteria: npm run build succeeds; no files with zero imports (except intentional barrels).
Phase 1 — Boundary Enforcement (Weeks 2–3)
Goal: Prevent architectural regression as new modules are added.
| Task | Description | Effort |
|---|---|---|
Add ESLint no-restricted-imports rules |
Block @/components/accounting/* from beauty/healthcare and vice versa |
M |
| Add ESLint rule: DS must not import domain code | Already clean — add rule to keep it that way | S |
| Document allowed import matrix | See module-boundaries.md | S |
| Add path aliases (optional) | @/ds/*, @/platform/* — reduce @/ ambiguity |
M |
| Consolidate beauty owner routes | Merge beauty/(owner)/* CRUD into components/beauty/pages/owner.tsx pattern OR merge owner/ into (owner)/ |
L |
Exit criteria: Lint fails on cross-domain imports; beauty owner has single URL tree.
Phase 2 — Shared Portal Infrastructure (Weeks 4–6)
Goal: Extract beauty/healthcare duplication into reusable platform layer.
2.1 Create components/portals/ (shared)
Extract from duplicated files:
| Shared abstraction | Source files |
|---|---|
createPortalLayout() |
beauty/createPortalLayout.tsx, healthcare/createPortalLayout.tsx |
PortalShell base |
BeautyPortalShell, HealthcarePortalShell, AccountingShell (~90% overlap) |
pages/shared.tsx helpers |
beauty/pages/shared.tsx, healthcare/pages/shared.tsx |
PublicSiteLayout |
PublicBeautyLayout, PublicHealthcareLayout |
| Accent-token cards/chips | BeautyCards, HealthcareCards, BeautyStatusChip, MedicalStatusChip |
Parameterize by:
- Portal ID type (generic)
- Nav config (
lib/*-portals.tsstays domain-specific) - CSS accent variable (
--beauty-accentvs--health-accent)
2.2 Normalize module DS folder naming
Pick one convention:
- Option A (recommended):
components/{module}/design-system/for both beauty and healthcare - Option B: Move module-specific tokens to
components/portals/tokens.tswith domain presets
Exit criteria: Single createPortalLayout factory; portal shells share one base component; healthcare ui/ renamed to design-system/.
Phase 3 — API Client Decomposition (Weeks 7–10)
Goal: Split monolithic API clients into maintainable modules.
Current state
| File | Lines | Methods (approx) |
|---|---|---|
lib/accounting-api.ts |
1,856 | 200+ |
lib/beauty-business-api.ts |
1,446 | 150+ |
lib/healthcare-api.ts |
1,322 | 120+ |
Target structure
lib/
├── api/
│ ├── core.ts # Shared fetch wrapper, error class, token refresh
│ ├── accounting/
│ │ ├── index.ts # Re-export barrel
│ │ ├── vouchers.ts
│ │ ├── treasury.ts
│ │ ├── inventory.ts
│ │ └── ...
│ ├── beauty/
│ │ ├── index.ts
│ │ ├── appointments.ts
│ │ ├── catalog.ts
│ │ └── ...
│ └── healthcare/
│ ├── index.ts
│ └── ...
├── accounting-api.ts # Deprecated barrel → re-exports from api/accounting/
Migration strategy:
- Extract shared
createApiClient(baseUrl, serviceName)helper - Move one domain subdomain at a time (e.g.
accounting/vouchers) - Keep deprecated barrel exports until all imports updated
- Run grep after each subdomain to verify zero old imports
Exit criteria: No API file exceeds 400 lines; shared fetch logic in one place.
Phase 4 — Accounting Route Extraction (Weeks 11–16)
Goal: Align accounting with beauty/healthcare thin-route pattern.
Priority extractions (largest route files)
| Route file | Lines | Target component |
|---|---|---|
app/accounting/chart-of-accounts/page.tsx |
644 | components/accounting/ChartOfAccountsPage.tsx |
app/accounting/reports/page.tsx |
663 | components/accounting/ReportsPage.tsx |
app/accounting/fiscal/page.tsx |
579 | components/accounting/FiscalPage.tsx |
app/accounting/vouchers/[id]/page.tsx |
550 | components/accounting/VoucherDetailPage.tsx |
app/accounting/treasury/page.tsx |
490 | components/accounting/TreasuryPage.tsx |
app/accounting/payroll/page.tsx |
457 | components/accounting/PayrollPage.tsx |
Screen aggregator split
| Current file | Lines | Action |
|---|---|---|
BusinessDocsPage.tsx |
1,328 | Split by document type |
WorkflowScreens.tsx |
1,132 | Split by workflow domain |
OperationalDocumentPage.tsx |
996 | Keep; already reusable |
DomainScreens.tsx |
652 | Split by domain section |
Pattern:
// app/accounting/reports/page.tsx (after)
export { ReportsPage as default } from "@/components/accounting/ReportsPage";
Exit criteria: No accounting page.tsx exceeds 50 lines except dynamic route params; no component exceeds 500 lines.
Phase 5 — Platform Hardening (Weeks 17–20)
Goal: Production-grade resilience and auth.
| Task | Description |
|---|---|
Add middleware.ts |
Edge auth check, tenant header injection, public route allowlist |
Add segment error.tsx |
Per-module error boundaries (accounting/, beauty/, healthcare/) |
Add segment loading.tsx |
Skeleton states using DS LoadingState / Skeleton |
| Conditional SiteHeader | Hide platform header on /beauty/site/*, /healthcare/site/* |
| BFF for Core API | /api/core/[...path] proxy for admin/dashboard |
| Bundle analysis | @next/bundle-analyzer baseline before module #4 |
Phase 6 — New Module Template (Ongoing)
When adding module #4 (e.g. Delivery, CRM, Hospitality):
Required scaffold
app/{module}/
├── layout.tsx # pass-through or module shell
├── hub/page.tsx # portal picker
├── {portal}/layout.tsx # createPortalLayout("{portal}")
├── {portal}/.../page.tsx # thin re-exports
└── site/layout.tsx # public site (if applicable)
components/{module}/
├── design-system/
│ ├── tokens.ts
│ └── index.ts
├── pages/
│ ├── shared.tsx
│ └── {portal}.tsx
├── {Module}PortalShell.tsx # extends shared PortalShell
└── createPortalLayout.tsx # OR import from components/portals/
lib/
├── {module}-api.ts # OR lib/api/{module}/
└── {module}-portals.ts
app/api/{module}/[...path]/route.ts
Checklist before merge
- Zero imports from other business modules
- All pages use
@/components/ds(notcomponents/ui) - BFF proxy route exists
- Nav config in
lib/{module}-portals.ts - Query keys prefixed
["{module}", tenantId, ...] - ESLint boundary rules pass
Phase Summary Timeline
Phase 0 ████ Stabilize (Week 1)
Phase 1 ░░░░████ Boundaries (Weeks 2–3)
Phase 2 ░░░░░░░░████████ Portal infra (Weeks 4–6)
Phase 3 ░░░░░░░░░░░░░░████████████ API clients (Weeks 7–10)
Phase 4 ░░░░░░░░░░░░░░░░░░░░████████████████ Accounting routes (Weeks 11–16)
Phase 5 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░████████ Hardening (Weeks 17–20)
Phase 6 ────────────────────────────────────► Module template (ongoing)
What NOT to do
| Anti-pattern | Reason |
|---|---|
| Split into npm packages prematurely | Team size doesn't justify overhead yet |
| Introduce Redux/Zustand | TanStack Query + local state is sufficient |
| Rewrite accounting in beauty page-bundle style in one PR | Too risky; extract file-by-file |
| Move public sites to separate Next.js app | Shared auth/tenant infra makes monolith correct for now |
| Adopt TanStack Table without requirement | Custom DataTable works; add features when needed |
Success Metrics
| Metric | Current | Phase 0 | Phase 4 | Phase 5 |
|---|---|---|---|---|
| Build passes | ❌ | ✅ | ✅ | ✅ |
| Max page.tsx lines (accounting) | 663 | 663 | ≤50 | ≤50 |
| Max component file lines | 1,856 | 1,856 | ≤500 | ≤500 |
| Cross-domain imports | 0 | 0 | 0 | 0 (enforced) |
| Dead files | 5 | 0 | 0 | 0 |
| Unused npm deps | 2 | 0 | 0 | 0 |
| Portal factory implementations | 2 | 2 | 1 | 1 |
| Middleware coverage | 0% | 0% | 0% | 100% public/private |