Skip to content

Monorepo Migration Design

[S1] Problem

The current frontend is a single TanStack Start SPA in frontend/. We need to create an admin panel for user management and analytics, reusing existing infrastructure. A monorepo approach allows shared code (UI components, API layer, hooks) while keeping apps independent for deployment.

[S2] Solution Overview

Transform frontend/ into a Bun workspaces monorepo with Turborepo for build orchestration. Create shared packages for reusable code, and separate apps for the main SPA and admin panel.

Structure

frontend/                          # Monorepo root
├── packages/
│   ├── ui/                        # shadcn/ui components, styles, hooks
│   ├── api/                       # Orval-generated hooks, query keys, invalidation
│   ├── shared/                    # Auth helpers, session management, utils
│   └── editor/                    # TipTap editor components
├── apps/
│   ├── web/                       # Current TanStack Start SPA (moved from frontend/src/)
│   └── admin/                     # New admin SPA (React + Vite)
├── package.json                   # Root workspace config
├── turbo.json                     # Turborepo pipeline config
└── bun.lock                       # Lock file

Backend (unchanged)

django-boilerplate/
├── apps/                          # Django backend (not touched)
├── assets/                        # Django templates (not touched)
└── ...

[S3] Shared Packages

packages/ui

Purpose: Reusable UI components, styles, and design tokens.

Contents:

  • All shadcn/ui components (components/ui/)
  • App-specific UI components (components/circle/, components/events/, etc.)
  • CSS variables and Tailwind configuration
  • Hooks: useTheme, useDebounce, use-mobile
  • Utils: cn(), formatName(), formatDate()

Exports:

json
{
  "exports": {
    ".": "./src/index.ts",
    "./styles": "./src/styles.css"
  }
}

Dependencies: React, Tailwind CSS, Radix UI primitives, CVA, clsx, tailwind-merge

packages/api

Purpose: Orval-generated API client and query management.

Contents:

  • generated.ts (Orval output)
  • hooks.ts (composed hooks)
  • queries.ts (query key factories)
  • invalidation.ts (mutation invalidation helpers)
  • mutator.ts (custom fetch wrapper)

Exports:

json
{
  "exports": {
    ".": "./src/index.ts"
  }
}

Dependencies: @tanstack/react-query, Orval runtime

packages/shared

Purpose: Core utilities, auth helpers, and server-side logic.

Contents:

  • auth.ts (authentication helpers)
  • session.server.ts (Upstash Redis session management)
  • config.server.ts (environment config)
  • error-capture.ts (error handling)
  • utils.ts (general utilities)

Exports:

json
{
  "exports": {
    ".": "./src/index.ts",
    "./server": "./src/server.ts"
  }
}

Dependencies: @upstash/redis, zod

packages/editor

Purpose: TipTap rich text editor components.

Contents:

  • TipTapEditor.tsx, TipTapEditorLazy.tsx
  • EditorToolbar.tsx
  • MentionList.tsx
  • Custom extensions

Exports:

json
{
  "exports": {
    ".": "./src/index.ts"
  }
}

Dependencies: @tiptap/react, @tiptap/starter-kit, TipTap extensions

[S4] Apps

apps/web (Current SPA)

Framework: TanStack Start (React 19 + TanStack Router + SSR)

Port: 3000

Deploy: Cloudflare Workers

Changes:

  • Move frontend/src/apps/web/src/
  • Move frontend/public/apps/web/public/
  • Move frontend/server.ts, start.ts, router.tsxapps/web/
  • Update imports to use @repo/ui, @repo/api, etc.
  • Update vite.config.ts to reference workspace packages
  • Keep existing wrangler.jsonc (if exists) or create new one

Scripts:

json
{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "generate:api": "curl -s http://localhost:8000/api/schema/ > ../api/openapi.json && bunx orval"
  }
}

apps/admin (New Admin SPA)

Framework: React 19 + Vite + TanStack Router + TanStack Query (no SSR)

Port: 3001

Deploy: Cloudflare Workers (separate worker)

Scope:

  • User management (list, ban, change roles, view activity)
  • Analytics and reports (usage metrics, engagement, growth)

Routes:

/admin                    # Dashboard with metrics
/admin/users              # User list with filters
/admin/users/:id          # User detail
/admin/analytics          # Reports and charts

Structure:

apps/admin/
├── src/
│   ├── routes/
│   │   ├── __root.tsx
│   │   ├── index.tsx          # Dashboard
│   │   ├── users.tsx          # User list
│   │   ├── users.$userId.tsx  # User detail
│   │   └── analytics.tsx      # Analytics
│   ├── components/
│   │   ├── Dashboard/
│   │   ├── Users/
│   │   └── Analytics/
│   ├── main.tsx
│   └── styles.css
├── package.json
├── vite.config.ts
├── tsconfig.json
└── wrangler.jsonc

Scripts:

json
{
  "scripts": {
    "dev": "vite --port 3001",
    "build": "vite build",
    "preview": "vite preview",
    "deploy": "wrangler deploy"
  }
}

[S5] Authentication

Both apps share the same JWT authentication:

  • Token storage: Upstash Redis-backed sessions (via packages/shared)
  • Token refresh: Automatic refresh via /api/token/refresh/
  • User context: Same CustomUser model, same Membership resolution
  • API calls: Same X-Community-Slug header for tenant resolution

Admin-specific auth:

  • Check if user has is_staff or is_superuser flag on CustomUser model before accessing /admin/* routes
  • Redirect non-admin users to main app (/)
  • Use same JWT token, just different route protection
  • Admin routes protected via ProtectedRoute component with role check

[S6] Build & Development

Turborepo Pipeline

json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {},
    "type-check": {
      "dependsOn": ["^build"]
    }
  }
}

Root Scripts

json
{
  "scripts": {
    "dev": "turbo dev",
    "dev:web": "turbo dev --filter=web",
    "dev:admin": "turbo dev --filter=admin",
    "build": "turbo build",
    "build:web": "turbo build --filter=web",
    "build:admin": "turbo build --filter=admin",
    "lint": "turbo lint",
    "type-check": "turbo type-check",
    "generate:api": "cd packages/api && bun run generate"
  }
}

Development Workflow

bash
# Start all apps
bun dev

# Start only web
bun dev:web

# Start only admin
bun dev:admin

# Build all
bun build

# Generate API client
bun generate:api

[S7] Deployment

Cloudflare Workers

Each app deploys independently:

apps/web:

  • Worker name: web
  • URL: web.seudominio.com
  • Deploy: cd apps/web && bun run deploy

apps/admin:

  • Worker name: admin
  • URL: admin.seudominio.com
  • Deploy: cd apps/admin && bun run deploy

Makefile Updates

makefile
# TanStack SPA (current)
f-dev:           # Start web dev server
f-build:         # Build web for production

# Admin SPA (new)
f-admin-dev:     # Start admin dev server
f-admin-build:   # Build admin for production

# Monorepo
f-install:       # Install all dependencies
f-lint:          # Lint all packages
f-type-check:    # Type check all packages

[S8] Migration Steps

Important: Directory Handling

The current frontend/ directory becomes the monorepo root. We do NOT create a new directory. Instead:

  • frontend/src/ moves to frontend/apps/web/src/
  • frontend/public/ moves to frontend/apps/web/public/
  • Config files stay in frontend/ (root level)
  • New packages/ and apps/ directories are created inside frontend/

This minimizes git history disruption and keeps the same working directory.

Phase 1: Setup Monorepo Structure

  1. Create frontend/packages/ directory structure
  2. Create frontend/apps/web/ and frontend/apps/admin/ directories
  3. Move shared code from frontend/src/ to packages
  4. Update root frontend/package.json with workspaces config
  5. Create frontend/turbo.json
  6. Verify existing code still works (no import changes yet)

Phase 2: Move Web App

  1. Move frontend/src/frontend/apps/web/src/
  2. Move frontend/public/frontend/apps/web/public/
  3. Move frontend/server.ts, start.ts, router.tsxfrontend/apps/web/
  4. Move frontend/vite.config.tsfrontend/apps/web/vite.config.ts
  5. Move frontend/tsconfig.jsonfrontend/apps/web/tsconfig.json
  6. Update all imports to use @repo/ui, @repo/api, etc.
  7. Verify web app works (cd apps/web && bun dev)

Phase 3: Create Admin App

  1. Scaffold frontend/apps/admin/ with Vite + React
  2. Setup TanStack Router and Query
  3. Create basic layout and auth protection
  4. Implement user management features
  5. Implement analytics features

Phase 4: Deploy

  1. Create wrangler.jsonc for admin
  2. Update root Makefile with new commands
  3. Test deployment for both apps
  4. Update DNS for admin subdomain

[S9] Risks & Mitigations

Risk: Import path breakage

Mitigation: Use TypeScript path aliases (@repo/ui, @repo/api) and verify with type-check after each phase.

Risk: Circular dependencies between packages

Mitigation: Clear dependency hierarchy:

  • ui depends on nothing (or only React)
  • shared depends on nothing
  • api depends on shared
  • editor depends on ui
  • Apps depend on all packages

Risk: Build caching issues

Mitigation: Use Turborepo's --force flag when needed, and clear .turbo cache if builds are stale.

Risk: Auth token sync between apps

Mitigation: Both apps use same Redis session store. Tokens are independent but validated against same backend.

[S10] Success Criteria

  • [ ] Web app works unchanged after migration
  • [ ] Admin app can login with same credentials
  • [ ] Admin app can list users and view details
  • [ ] Admin app can display analytics charts
  • [ ] Both apps deploy independently to Cloudflare Workers
  • [ ] bun dev starts both apps concurrently
  • [ ] bun build builds all packages and apps
  • [ ] No circular dependency errors
  • [ ] TypeScript type checking passes for all packages

Strum — Documentação.