Por qué un solo repo
Farcholabs funciona con tres piezas: el sitio público, un admin con el CRM y una API con un worker en segundo plano. El sitio y el admin son una sola app de Next.js. La API es Python, con FastAPI y MongoDB. Todo vive en un solo repo.
La otra opción era un repo para el sitio y otro para la API. Cuando empezamos dejamos escrito por qué no la tomamos: un cambio en el contrato entre los dos tocaría dos repos y necesitaría dos pull requests, y mientras tanto el admin y la API quedarían desfasados. En un solo repo, el cambio que agrega un campo a la API también lo agrega a la pantalla que lo muestra, en el mismo pull request.
Contamos cuántas veces pasa. Desde el pull request que armó el repo (#3, 25 de septiembre de 2026) hasta el #121 (18 de octubre), integramos 119 pull requests. 37 cambiaron las dos apps y 29 cambiaron el archivo del contrato de la API.
| Solo la web | 59 PR |
|---|---|
| Solo la API | 10 PR |
| Ninguna (docs, infra) | 13 PR |
| Las dos apps | 37 PR |
Cerca de un tercio del trabajo cruzó la línea entre las apps. Con dos repos, cada uno de esos 37 habrían sido dos pull requests, integrados en el orden correcto.
De qué se encarga cada carpeta
Cada carpeta de primer nivel tiene un solo trabajo, y sus reglas están escritas en el CLAUDE.md del repo, así que una persona o un agente de IA las lee antes de cambiar algo.
No hay un paquete compartido entre las apps. Lo único que comparten las dos apps es un archivo: FastAPI escribe apps/api/openapi.json a partir de las rutas y los modelos de la API, y la web genera con él sus tipos de TypeScript en apps/web/lib/api/schema.d.ts. Un cambio en una ruta regenera los dos en el mismo commit, y CI falla si alguno quedó desactualizado.
docs/ guarda el plan, con las decisiones que solo toma el dueño listadas como puntos abiertos, y una ADR por cada decisión de arquitectura, desde el hosting hasta cómo se publica el blog. Cuando un agente escribe código aquí, esos archivos son lo primero que lee, junto al código.
Un CI que corre solo lo que cambió
Cada app tiene su workflow de GitHub Actions, y cada workflow lista las rutas que le importan. Un cambio en el admin corre los chequeos de la web. Un cambio en la API corre el linter de Python, el chequeo de tipos y los tests contra un MongoDB real. Ninguno espera al otro.
Antes de npm run build, el workflow de la web revisa los tipos generados de la API, los tipos y el lint, el contraste de cada par de colores en los dos temas, el largo del título y la descripción de cada página, que cada idioma tenga las mismas páginas y que las figuras de cada artículo sean seguras.
name: web
on:
pull_request:
paths:
- "apps/web/**"
- "apps/api/openapi.json"
- ".nvmrc"
- ".github/workflows/web.yml"
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/webLa segunda ruta une a las dos: el workflow de la web también vigila el archivo del contrato de la API, así que un cambio en la API que toca el contrato corre los dos workflows, y la web comprueba que sus tipos generados siguen coincidiendo. La regla de GitHub es que un workflow corre cuando al menos un archivo cambiado coincide (en inglés) con una de sus rutas.
| El cambio | web.yml | api.yml |
|---|---|---|
apps/web/** | corre | no corre |
apps/api/**, sin tocar el contrato | no corre | corre |
apps/api/openapi.json | corre | corre |
docs/, infra/, tools/ | no corre | no corre |

Un detalle
Según GitHub, un workflow que se salta por su filtro de ruta deja sus chequeos en "Pending", y un pull request que los exige no se puede integrar. Si los vuelves obligatorios, piensa en los pull requests que se los saltan.
Un comando para el backend
docker compose up --build levanta MongoDB, la API y el worker. MongoDB corre como un replica set de un nodo, porque la API escribe en varias colecciones dentro de una transacción y un servidor MongoDB simple no puede hacerlo. CI lo levanta igual.
services:
mongo:
image: mongo:8
command: ["--replSet", "rs0", "--bind_ip_all"]
api:
build: ./apps/api
ports:
- "8000:8000"
worker:
build: ./apps/api
command: ["python", "-m", "app.worker"]La API y el worker se construyen desde la misma carpeta. El worker es el mismo código arrancado con otro comando, así que una tarea y la ruta que la encola nunca quedan en versiones distintas. La web corre fuera de Docker, con npm run dev.
Se construye en cada merge, se despliega a pedido
GitHub Actions solo valida; nada ahí despliega. Los despliegues viven en infra/gcp, junto al código que publican:
- Con cada merge a main, Cloud Build construye dos imágenes, una desde
apps/weby otra desdeapps/api, marcadas con el commit, y no despliega nada. - Un segundo disparador, manual, corre el mismo archivo y después publica las dos en Cloud Run. Solo lo corre el dueño.
- Un script pequeño llena las especificaciones de Cloud Run con los valores del disparador y se niega a seguir si falta uno. Los secretos nunca pasan por él: las especificaciones los nombran en Secret Manager.
Las dos imágenes salen del mismo commit, así que la web y la API que salen juntas se probaron juntas, y nunca tenemos que llevar la cuenta de qué versión de la web funciona con qué versión de la API.
Qué queda fuera
Lo único que comparten las dos apps es un archivo.
El repo es un monorepo solo para esta plataforma. Cada producto que construye Farcholabs vive en su propio repo, con su propio pipeline, y se queda ahí. Traerlos aquí haría que el CI y el despliegue de cada producto dependieran de los del sitio de la empresa.
- Productos: cada uno en su repo.
- Secretos: en Secret Manager. Git ignora los archivos
.env, y cada app guarda solo los nombres en su.env.example. - Lo que nunca se despliega, como la herramienta de videos para redes, vive en
tools/, fuera de las dos imágenes.
Si estás armando un producto con una web y una API, nuestro servicio de plataformas web parte de esta misma estructura, o puedes ver qué te sirve primero.


