🗂️ ESTÁNDAR DE MONOREPO (Turborepo)
🎯 ¿Qué es y cuándo usarlo?
Un Monorepo es un repositorio único que contiene múltiples proyectos relacionados (workers, frontend, paquetes compartidos). Turborepo es el orquestador de builds elegido porque: tiene caché de builds inteligente (no rebuildea lo que no cambió), escala a 100+ paquetes, es Zero-Config con TypeScript y se integra nativamente con Cloudflare Workers.
[REQUIRED] REGLA: Todo código compartido entre ≥ 2 proyectos (helpers de response, tipos TypeScript, schemas Zod) vive en un paquete
packages/— NUNCA copiado entre proyectos. Copiar es crear deuda técnica garantizada. Ref: BACKEND_ENGINEERING_STANDARD.md §03 (módulo CORS compartido).Por qué: copiar un helper entre proyectos crea dos copias que empiezan idénticas y divergen en la primera corrección de bug que solo se aplica a una de ellas. Un paquete compartido garantiza que corregir en un lugar corrige en todos los consumidores a la vez.
🏗️ 1. ESTRUCTURA DE DIRECTORIOS
collabscribe/ ← Raíz del monorepo
├── package.json ← workspaces: ["apps/*", "packages/*"]
├── turbo.json ← Configuración de pipelines
├── pnpm-workspace.yaml ← pnpm workspaces
│
├── apps/ ← Aplicaciones desplegables
│ ├── web/ ← Frontend React (Cloudflare Pages)
│ ├── api-gateway/ ← Cloudflare Worker: punto de entrada
│ ├── auth-worker/ ← Cloudflare Worker: /api/auth/*
│ ├── docs-worker/ ← Cloudflare Worker: /api/documents/*
│ ├── teams-worker/ ← Cloudflare Worker: /api/teams/*
│ ├── notifs-worker/ ← Cloudflare Worker: /api/notifications/*
│ ├── search-worker/ ← Cloudflare Worker: /api/search/*
│ └── embeddings-worker/ ← Cloudflare Worker: Queue consumer
│
└── packages/ ← Código compartido (nunca desplegado directamente)
├── shared-types/ ← Tipos TypeScript compartidos (Document, Team, User...)
├── shared-schemas/ ← Schemas Zod compartidos (validación frontend + backend)
├── shared-http/ ← Helpers: ok(), fail(), getCorsHeaders(), rateLimit()
├── shared-db/ ← Cliente Supabase + helpers de queries
└── tsconfig/ ← tsconfig.json base compartido⚙️ 2. CONFIGURACIÓN BASE
turbo.json (Pipelines)
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // Buildear dependencias antes que el proyecto
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false, // Dev nunca se cachea
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"deploy": {
"dependsOn": ["build", "test", "typecheck"],
"outputs": []
}
}
}pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'package.json raíz
{
"name": "collabscribe",
"private": true,
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"deploy": "turbo run deploy"
},
"devDependencies": {
"turbo": "^2.0.0",
"typescript": "^5.4.0"
},
"packageManager": "pnpm@9.0.0"
}📦 3. PAQUETES COMPARTIDOS (El corazón del monorepo)
packages/shared-types — Tipos TypeScript
// packages/shared-types/src/index.ts
// Estos tipos son importados por el frontend Y por cada worker
export type MemberRole = 'owner' | 'admin' | 'editor' | 'viewer'
export type DocStatus = 'backlog' | 'draft' | 'review' | 'published' | 'archived'
export interface Team {
id: string; name: string; slug: string
created_at: string; updated_at: string
}
export interface Document {
id: string; team_id: string; created_by: string
title: string; content: string
status: DocStatus; is_public: boolean
created_at: string; updated_at: string
}
export interface TeamMember {
team_id: string; user_id: string
role: MemberRole; invited_at: string
}
// Respuesta envelope estándar del API (API_ENGINEERING_STANDARD.md §04)
export interface ApiResponse<T> {
success: true; data: T
}
export interface ApiError {
success: false; error: { code: string; message: string }
}packages/shared-schemas — Schemas Zod
// packages/shared-schemas/src/documents.ts
import { z } from 'zod'
export const createDocumentSchema = z.object({
team_id: z.string().uuid(),
title: z.string().min(1).max(500).trim(),
content: z.string().max(5_000_000).default(''),
status: z.enum(['backlog', 'draft', 'review', 'published', 'archived']).default('draft')
})
export const updateDocumentSchema = createDocumentSchema.partial().omit({ team_id: true })
export type CreateDocumentInput = z.infer<typeof createDocumentSchema>
export type UpdateDocumentInput = z.infer<typeof updateDocumentSchema>packages/shared-http — Helpers de Response y CORS
// packages/shared-http/src/response.ts
// Ref: BACKEND_ENGINEERING_STANDARD.md §01 — helper único, no repetido en cada worker
export function ok<T>(data: T, corsHeaders?: HeadersInit): Response {
return new Response(JSON.stringify({ success: true, data }), {
status: 200,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
})
}
export function fail(
code: string,
message: string,
status: number,
corsHeaders?: HeadersInit
): Response {
return new Response(JSON.stringify({ success: false, error: { code, message } }), {
status,
headers: { 'Content-Type': 'application/json', ...corsHeaders }
})
}
// packages/shared-http/src/cors.ts
// Ref: BACKEND_ENGINEERING_STANDARD.md §03 — módulo compartido, NO copiado
const ALLOWED_ORIGINS: Record<string, string[]> = {
production: ['https://collabscribe.com', 'https://app.collabscribe.com'],
staging: ['https://staging.collabscribe.com'],
development: ['http://localhost:3000', 'http://localhost:5173']
}
export function getCorsHeaders(environment: string, origin: string | null): HeadersInit {
const allowed = ALLOWED_ORIGINS[environment] ?? []
const isAllowed = origin && allowed.includes(origin)
return {
'Access-Control-Allow-Origin': isAllowed ? origin : (allowed[0] ?? ''),
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
}🚀 4. FLUJO DE DEPLOY EN CI (GitHub Actions)
Turborepo detecta qué apps cambiaron y solo despliega esas.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install --frozen-lockfile
# Turborepo Remote Cache (evita re-buildear lo que ya está cacheado en el servidor de CI)
- name: Build
run: pnpm turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
# Desplegar cada Worker independientemente
- name: Deploy Workers
run: pnpm turbo run deploy --filter="./apps/*-worker"
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# Desplegar Frontend (Cloudflare Pages)
- name: Deploy Frontend
run: pnpm turbo run deploy --filter="./apps/web"📋 CHECKLIST DE SETUP
- [ ]
pnpmcomo package manager (no npm, no yarn — consistencia) - [ ]
turbo.jsoncon pipelinedeployque depende debuild + test + typecheck - [ ]
packages/shared-httpimportado por todos los workers (no copiado) - [ ]
packages/shared-schemasimportado por frontend Y workers (single source of truth) - [ ]
packages/shared-typesimportado por todos (coherencia de contratos) - [ ] Remote Cache de Turborepo configurado en CI (ahorra 5-10 min por deploy)
- [ ] Cada worker tiene su propio
wrangler.tomlcon bindings específicos - [ ]
.env.exampleen la raíz con todas las variables necesarias documentadas