K. Recommended Target Architecture
Target: Monorepo with npm Workspaces + Turborepo
Tooling: npm workspaces + Turborepo Node version: 24 LTS (unified; Active LTS as of May 2026) Language: TypeScript everywhere (reverb-backend migration phased) Package manager: npm (already used across all repos — no migration required)
Proposed Repository Structure
soreto-platform/
│
├── apps/
│ ├── platform-api/ ← reverb-backend (migrated, renamed)
│ │ ├── src/
│ │ │ ├── routes/api/
│ │ │ ├── services/
│ │ │ ├── middleware/
│ │ │ ├── config/
│ │ │ └── common/
│ │ ├── migrations/ ← stays here (reverb schema)
│ │ ├── test/
│ │ ├── Procfile ← web: node dist/server.js
│ │ ├── package.json
│ │ ├── tsconfig.json ← extends ../../tooling/tsconfig/base.json
│ │ ├── .env.example
│ │ └── CLAUDE.md
│ │
│ ├── platform-ui/ ← soreto-melissa (migrated)
│ │ ├── app/ ← Next.js 16 App Router (no src/)
│ │ │ ├── (admin)/ ← admin routes — guarded layout
│ │ │ ├── (client)/ ← client routes — CLIENT_USER, SAAS
│ │ │ ├── (full-page)/ ← auth, subscribe, public
│ │ │ ├── (landing)/ ← marketing
│ │ │ ├── layout.tsx ← root layout
│ │ │ └── page.tsx
│ │ ├── components/ ← shadcn/ui components + Tailwind CSS v4
│ │ │ └── ui/ ← added via npx shadcn@latest add <component>
│ │ ├── service/ ← typed API calls via @soreto/api-client
│ │ ├── shared/ ← constants, helpers
│ │ ├── store/ ← React Context / Zustand
│ │ ├── types/ ← local TypeScript definitions
│ │ ├── public/ ← static assets
│ │ ├── globals.css ← Tailwind v4 @theme directive (no tailwind.config.js)
│ │ ├── Procfile ← web: npm start
│ │ ├── package.json
│ │ ├── tsconfig.json ← extends ../../tooling/tsconfig/nextjs.json
│ │ ├── next.config.ts
│ │ ├── components.json ← shadcn/ui CLI config
│ │ ├── .env.example
│ │ └── CLAUDE.md
│ │
│ ├── zoe/ ← soreto-zoe (migrated, refactored)
│ │ ├── src/
│ │ │ ├── modules/
│ │ │ ├── services/
│ │ │ └── database/
│ │ ├── processors/ ← eventually typed
│ │ ├── migrations/ ← stays here (zoe schema)
│ │ ├── Procfile ← worker: node dist/worker.js
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── config.js ← replace with @soreto/config
│ │ └── CLAUDE.md
│ │
│ └── legacy-portal/ ← reverb-react (modernized → Next.js 16 + React 19 + Node 24)
│ ├── app/ ← Next.js 16 App Router (no src/)
│ │ ├── (admin)/ ← admin routes (ported from reverb-react admin components)
│ │ │ ├── dashboard/
│ │ │ ├── campaigns/
│ │ │ ├── users/
│ │ │ ├── reports/
│ │ │ └── layout.tsx
│ │ ├── (client)/ ← client routes (ported from reverb-react client components)
│ │ │ ├── dashboard/
│ │ │ ├── campaigns/
│ │ │ └── layout.tsx
│ │ ├── layout.tsx ← root layout (auth guard)
│ │ └── page.tsx ← root redirect
│ ├── components/ ← shadcn/ui components + Tailwind CSS v4
│ ├── store/ ← Zustand stores (replaces Redux 3 + Flux)
│ ├── service/ ← typed API calls via @soreto/api-client
│ ├── types/ ← TypeScript definitions
│ ├── public/ ← static assets (images)
│ ├── Procfile ← web: npm start
│ ├── package.json ← Next.js 16, React 19, TypeScript 6, Node 24
│ ├── tsconfig.json ← extends ../../tooling/tsconfig/nextjs.json
│ ├── next.config.ts
│ ├── .env.example
│ └── CLAUDE.md
│
├── packages/
│ ├── types/ ← @soreto/types
│ │ ├── src/
│ │ │ ├── entities/ ← Campaign, Order, Reward, SharedUrl, User
│ │ │ ├── enums/ ← RoleName, OrderStatus, RewardStatus
│ │ │ ├── api/ ← ApiResponse, PaginatedResponse
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── api-client/ ← @soreto/api-client
│ │ ├── src/
│ │ │ ├── client.ts ← ReverbClient class
│ │ │ ├── postReward.ts
│ │ │ ├── notification.ts
│ │ │ ├── zendesk.ts
│ │ │ └── index.ts
│ │ ├── test/
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── constants/ ← @soreto/constants
│ │ ├── src/
│ │ │ ├── roles.ts
│ │ │ ├── countries.ts
│ │ │ ├── campaignTypes.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── config/ ← @soreto/config
│ │ ├── src/
│ │ │ ├── loader.ts ← validated env loading
│ │ │ └── schemas/ ← per-app schemas
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── auth-utils/ ← @soreto/auth-utils
│ ├── src/
│ │ ├── cookieHandler.ts ← from soreto-cookie-jam
│ │ └── jwtDecode.ts
│ ├── package.json
│ └── tsconfig.json
│
├── tooling/
│ ├── eslint-config/ ← @soreto/eslint-config
│ │ ├── index.js
│ │ ├── next.js
│ │ ├── node.js
│ │ └── package.json
│ │
│ ├── tsconfig/ ← @soreto/tsconfig
│ │ ├── base.json
│ │ ├── nextjs.json
│ │ ├── node.json
│ │ └── package.json
│ │
│ └── jest-config/ ← @soreto/jest-config
│ ├── jest.base.js
│ ├── jest.node.js
│ ├── jest.react.js
│ └── package.json
│
├── docs/
│ ├── architecture/
│ │ ├── SYSTEM_OVERVIEW.md
│ │ ├── DATABASE_SCHEMA.md
│ │ ├── API_CONTRACT.md
│ │ └── AUTH_FLOW.md
│ ├── adr/
│ │ ├── 001-monorepo-migration.md
│ │ ├── 002-node-standardization.md
│ │ ├── 003-typescript-adoption.md
│ │ ├── 004-modernize-reverb-react.md
│ │ └── 005-seneca-decision.md
│ ├── spec/
│ │ ├── DOMAIN_MODEL.md
│ │ ├── API_ENDPOINTS.md
│ │ └── JOB_PROCESSORS.md
│ ├── migration/
│ │ ├── REVERB_REACT_PARITY.md
│ │ └── NODE_UPGRADE_TRACKER.md
│ └── dependencies/
│ ├── INTER_REPO_COUPLING.md
│ └── UPGRADE_BLOCKERS.md
│
├── turbo.json ← Turborepo task pipeline
├── package.json ← Root workspace (engines, scripts, workspaces field)
├── .nvmrc ← 24 (Active LTS)
├── .env.example ← Shared env var documentation
└── CLAUDE.md ← Root AI context document
System Architecture Diagram
╔═══════════════════════════════════════════════════════════════════╗
║ SORETO PLATFORM ║
║ ║
║ ┌─────────────────┐ ┌───────────────────────────────────┐ ║
║ │ platform-ui │────►│ platform-api │ ║
║ │ (Next.js 16) │ │ (Express 5, Node 24, TypeScript) │ ║
║ │ React 19 │ │ │ ║
║ │ shadcn/ui │ │ Routes: /api/v1/* │ ║
║ │ TypeScript │ │ Auth: Passport (cookie/bearer) │ ║
║ └─────────────────┘ └────────────┬──────────────────────┘ ║
║ │ ║
║ ┌─────────────────┐ │ ║
║ │ legacy-portal │ ┌─────────────▼─────────┐ ║
║ │ (→ Next.js 16) │ │ PostgreSQL │ ║
║ │ reverb-react │ │ schema: reverb │ ║
║ └─────────────────┘ │ schema: zoe │ ║
║ └─────────────▲─────────┘ ║
║ ┌─────────────────┐ │ ║
║ │ zoe │────────────────────┘ ║
║ │ (Bull queues) │ ┌──────────────────────┐ ║
║ │ TypeScript │────►│ Redis │ ║
║ │ 12+ affiliate │ │ Sessions + Bull Q │ ║
║ │ processors │ └──────────────────────┘ ║
║ └─────────────────┘ ║
║ ║
║ ─ ─ ─ ─ ─ ─ ─ ─ shared packages ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ║
║ ║
║ @soreto/types @soreto/api-client @soreto/constants ║
║ @soreto/config @soreto/auth-utils @soreto/eslint-config ║
╚═══════════════════════════════════════════════════════════════════╝
Key Configuration Files
npm-workspaces (in package.json)
{
"workspaces": [
"apps/*",
"packages/*",
"tooling/*"
]
}
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"typecheck": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Root package.json
{
"name": "soreto-platform",
"private": true,
"engines": {
"node": ">=24.0.0",
"npm": ">=10.0.0"
},
"workspaces": [
"apps/*",
"packages/*",
"tooling/*"
],
"scripts": {
"build": "turbo build",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"dev": "turbo dev --parallel"
}
}
Migration Sequence
Apps migrate into this structure one at a time, not simultaneously. See L — Migration Plan for the full phased sequence.
Order:
1. Create the monorepo scaffold (empty structure)
2. Migrate soreto-melissa → apps/platform-ui/ (first — cleanest codebase)
3. Extract @soreto/types and @soreto/constants
4. Move reverb-react → apps/legacy-portal/ (stand up Next.js 16 scaffold; rewrite begins)
5. Migrate reverb-backend → apps/platform-api/
6. Migrate soreto-zoe → apps/zoe/ (after AMD TypeScript refactor)
7. Extract @soreto/api-client (replace local_modules/reverb)
Legacy Portal: Modernization Target
apps/legacy-portal/ (reverb-react) is modernized, not frozen. The existing stack (Babel 6, Webpack 4, Node 10, React 17/dom 16.5 mismatch, react-router@3 + react-router-dom@4 mismatch) cannot be incrementally upgraded — the gaps are too large and compound. The correct path is a controlled rewrite to Next.js 16 + React 19 inside the monorepo.
Modernize to Next.js 16
Target stack:
| Layer | Current | Target |
|---|---|---|
| Framework | CRA-style Webpack 4 bundle | Next.js 16.2.6 (App Router) |
| React | 17.0.1 + react-dom 16.5.0 ⛔ | React 19.2.6 |
| Routing | react-router@3 + react-router-dom@4 ⛔ | react-router@7.15.1 (or Next.js file routing) |
| Language | JavaScript/JSX | TypeScript 6.0.3 |
| State | Redux 3.7.2 + Flux (mixed) | React Context + Zustand or Redux Toolkit 2.x |
| Bundler | Webpack 4.7.0 | Turbopack (Next.js 16 default) |
| Node | ~10.15.3 ⛔ | 24 LTS |
| Styling | PrimeReact 1.5.1 | shadcn/ui (CLI v4) + Tailwind CSS 4.2.0 (matches platform-ui) |
| Build config | .babelrc (Babel 6) |
tsconfig.json + SWC |
Target structure after modernization:
apps/legacy-portal/ ← Next.js 16 App Router — no src/ directory
├── app/
│ ├── (admin)/ ← admin routes (guarded layout)
│ │ ├── dashboard/
│ │ ├── campaigns/
│ │ ├── users/
│ │ ├── reports/
│ │ └── layout.tsx
│ ├── (client)/ ← client routes (guarded layout)
│ │ ├── dashboard/
│ │ ├── campaigns/
│ │ └── layout.tsx
│ ├── layout.tsx ← root layout
│ └── page.tsx ← root redirect
├── components/ ← shadcn/ui components (CLI v4, vendored)
│ └── ui/ ← added via npx shadcn@latest add <component>
├── store/ ← Zustand stores (replaces Redux 3 + Flux)
├── service/ ← typed API calls via @soreto/api-client
├── types/ ← TypeScript definitions
├── public/ ← static assets
├── globals.css ← Tailwind v4 @theme directive (no tailwind.config.js)
├── package.json ← Next.js 16, React 19, TypeScript 6, Node 24
├── tsconfig.json ← extends ../../tooling/tsconfig/nextjs.json
├── next.config.ts
├── components.json ← shadcn/ui CLI config
├── .env.example
└── CLAUDE.md
Effort estimate: 6–10 weeks for one frontend engineer. This is a controlled rewrite — Babel 6, Flux, and the react-router v3/v4 dual installation cannot be incrementally migrated. The business logic (Redux action creators, API calls, reducers) can be preserved; the rendering and tooling layers are rewritten.
While the rewrite is in progress: Deploy a WAF (Cloudflare or AWS WAF) in front of the existing reverb-react Heroku app to compensate for Node 10 EOL exposure during the transition window.
Heroku Deployment Configuration
All four apps continue to run on Heroku. The monorepo changes where the code lives, not how it deploys. Each Heroku app gets one extra buildpack and one extra config var — that is the only Heroku-side change required.
Buildpack setup (per Heroku app)
Add heroku-buildpack-monorepo as the first buildpack, followed by the standard Node.js buildpack:
# platform-api
heroku buildpacks:clear -a <platform-api-app>
heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo -a <platform-api-app>
heroku buildpacks:add heroku/nodejs -a <platform-api-app>
heroku config:set APP_BASE=apps/platform-api -a <platform-api-app>
# platform-ui
heroku buildpacks:clear -a <platform-ui-app>
heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo -a <platform-ui-app>
heroku buildpacks:add heroku/nodejs -a <platform-ui-app>
heroku config:set APP_BASE=apps/platform-ui -a <platform-ui-app>
# legacy-portal
heroku buildpacks:clear -a <legacy-portal-app>
heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo -a <legacy-portal-app>
heroku buildpacks:add heroku/nodejs -a <legacy-portal-app>
heroku config:set APP_BASE=apps/legacy-portal -a <legacy-portal-app>
# zoe
heroku buildpacks:clear -a <zoe-app>
heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo -a <zoe-app>
heroku buildpacks:add heroku/nodejs -a <zoe-app>
heroku config:set APP_BASE=apps/zoe -a <zoe-app>
How the build works
1. heroku-buildpack-monorepo runs npm install from the REPO ROOT
└─ resolves all @soreto/* workspace packages
2. heroku/nodejs buildpack changes into APP_BASE directory
└─ runs the app's own build script (next build / tsc)
└─ starts the app via its Procfile
Procfile location
Each Procfile lives inside its app directory — not at the monorepo root:
| App | Procfile location | Content |
|---|---|---|
| platform-api | apps/platform-api/Procfile |
web: node dist/server.js |
| platform-ui | apps/platform-ui/Procfile |
web: npm start |
| legacy-portal | apps/legacy-portal/Procfile |
web: npm start |
| zoe | apps/zoe/Procfile |
worker: node dist/worker.js |
devDependency pruning — one thing to watch
Heroku's Node.js buildpack prunes devDependencies after build. With npm workspaces, ensure each app's package.json lists its runtime @soreto/* dependencies directly in dependencies (not only devDependencies). If a workspace package is only in devDependencies, the prune step will drop it and the app will fail to start.
// apps/platform-ui/package.json — correct
{
"dependencies": {
"@soreto/types": "*",
"@soreto/api-client": "*"
}
}
What stays exactly the same
| Item | No change required |
|---|---|
| Number of Heroku apps | Still 4 apps |
| Heroku pipelines and review apps | Unchanged |
| Config vars / environment variables | Per-app, unchanged |
| PostgreSQL, Redis add-ons | Unchanged |
| Heroku region and dyno type | Unchanged |
| Heroku CI (if used) | Update app.json to set APP_BASE per pipeline |
What Does Not Change
| Item | Reason |
|---|---|
Database schemas (reverb, zoe) |
Schema changes are separate from repo structure |
| PostgreSQL / Redis / RabbitMQ | Infrastructure stays the same |
| Heroku apps (count and names) | 4 apps stay on Heroku — buildpack config is the only change |
| Existing git history | Each app preserves its history via git subtree or history import |
| Business logic | No business logic changes during the structural migration |