Sistema de gestión de RRHH compuesto por dos aplicaciones independientes que conviven en este monorepo:
backend/— API Laravel 13 sobre PHP 8.5, Postgres 18, Redis 7 y Mailpit.frontend/— App Next.js 16 + React 19 + NextAuth v5.
Ambas mitades tienen su propio stack Docker, sus propias dependencias y su propio CLAUDE.md con reglas detalladas. Este README documenta el uso conjunto y el script orquestador stack.sh que vive en la raíz.
featureapp-stack/
├── stack.sh # ← orquestador único (este README lo documenta)
├── CLAUDE.md # mapa general para Claude Code
├── backend/
│ ├── app-laravel/ # aplicación Laravel (CLAUDE.md profundo aquí)
│ ├── docker/ # imágenes PHP / nginx / postgres / redis / mailpit
│ ├── docker-compose.dev.yml # compose del backend
│ ├── README.md # detalles backend-only
│ └── .claude/rules/ # gates de arquitectura / hardening / testing
└── frontend/
├── app-next/ # aplicación Next.js (CLAUDE.md profundo aquí)
├── docker/
│ ├── .env # variables de runtime del frontend (NO commitear)
│ ├── .env.example
│ └── compose/
│ ├── docker-compose.yml # producción (frontend + nginx)
│ └── docker-compose.dev.yml # desarrollo (autocontenido, hot-reload)
└── INIT.md # detalles frontend-only
No hay un repo git en la raíz:
backend/yfrontend/son working trees independientes. Si vas a modificar código, leé elCLAUDE.mddel lado correspondiente.
- El frontend nunca toca la base de datos. Habla con Laravel únicamente a través de
frontend/app-next/lib/api/api-client.ts. - Auth: Laravel
/logindevuelveaccessToken→ NextAuth lo guarda en el JWT →apiClientlo agrega comoAuthorization: Bearer <token>. - Toda ruta del API se compone desde
${NEXT_PUBLIC_API_URL}(default:http://localhost:8081/api). Si cambiás la base del API en Laravel, actualizá esa var (es build-time: se inlinea en el bundle del cliente). - Casing en el borde: Laravel emite
snake_casey los mappers enfeatures/<domain>/mappers/convierten acamelCaseantes de que cualquier otra capa los vea.
- Docker Engine ≥ 24
- Docker Compose v2 (
docker compose ...) - Bash (el script usa
set -euo pipefail) - ~4 GB libres para imágenes y volúmenes
No hace falta PHP, Node ni Postgres en el host: todo corre en contenedores.
# .env de Laravel (lo lee la app dentro del contenedor)
cp backend/app-laravel/.env.example backend/app-laravel/.env
# .env del compose (puertos del host, UID/GID, credenciales de Postgres)
# Si no existe, copialo de la versión committeada de ejemplo o creá uno con
# las variables de la tabla "Variables del compose" más abajo.
ls backend/.env.docker || echo "→ crear backend/.env.docker antes del primer up"stack.sh crea backend/app-laravel/.env automáticamente si falta, pero falla con un mensaje claro si backend/.env.docker no existe (es el archivo que pasa a docker compose --env-file y define puertos/UID).
cp frontend/docker/.env.example frontend/docker/.envEditá frontend/docker/.env con tus valores. Mínimo:
NEXT_PUBLIC_API_URL=http://host.docker.internal:8081/api
NEXT_VERSION=latest
NEXT_PUBLIC_SIMULATE_DELAY=false
NEXT_PUBLIC_DELAY_MS=0
stack.shfalla con un mensaje claro sifrontend/docker/.envno existe (ese archivo no se puede autogenerar).
Conectividad browser → API: en dev,
NEXT_PUBLIC_API_URLse inlinea en el bundle del browser (corre en el host), así que la URL debe ser alcanzable desde el host:http://localhost:8081/api. El valorhost.docker.internal:8081solo sirve para fetches server-side desde el contenedor. La app vanilla no llama al API, pero al traer tu frontend real ajustáfrontend/app-next/.env.localaNEXT_PUBLIC_API_URL=http://localhost:8081/api.
./stack.sh build # construye ambas imágenes
# (backend: si app-laravel/ está vacío,
# instala y configura Laravel al terminar el build)
./stack.sh up # levanta backend + frontend
# (el entrypoint del backend corre las
# migraciones contra Postgres al arrancar)
./stack.sh shell app --backend # opcional: cargar datos de ejemplo
php artisan db:seed
exitBootstrap automático del backend: al hacer
./stack.sh build, sibackend/app-laravel/está vacío, se instalalaravel/laravel:${LARAVEL_VERSION}(default^13.0, configurable enbackend/.env.docker) en un contenedor efímero que escribe enapp-laravel/del host, con el.envcableado a Postgres/Redis/Mailpit yAPP_KEYgenerada. Es idempotente: si la app ya existe, no toca nada. Elentrypointrepite la instalación como fallback si se haceupsin haber buildeado antes.Las migraciones corren contra Postgres (no SQLite): la instalación usa
--no-scriptspara no migrar en SQLite en build-time, y elentrypointejecutaphp artisan migrate --forceal arrancar el contenedor web, cuando Postgres ya estáhealthy. Se puede desactivar conAUTO_MIGRATE=falseenbackend/.env.docker. El seeding queda manual (php artisan db:seed).
Bootstrap automático del frontend: al hacer
./stack.sh build(oup), sifrontend/app-next/no tienepackage.json, se scaffoldea Next.js (create-next-app@${NEXT_VERSION}, defaultlatest: TS · App Router · Tailwind · ESLint) antes del build, en un contenedornode:22-alpineefímero que escribe enapp-next/del host con los permisos del usuario (UID del host). Es idempotente: si ya haypackage.json, no toca nada. Deja elpnpm-lock.yamlpara que el arranque dev (pnpm install --frozen-lockfile) tenga contra qué congelar. Para forzar un scaffold nuevo, borráapp-next/package.jsony volvé abuild.
Servicios disponibles tras el primer up:
| URL | Qué es |
|---|---|
| http://localhost:8081 | API Laravel (nginx → php-fpm) — APP_PORT en backend/.env.docker |
| http://localhost:3000 | Frontend Next.js (modo dev) |
| http://localhost:8027 | Mailpit UI — FORWARD_MAILPIT_DASHBOARD_PORT en backend/.env.docker |
localhost:5435 |
Postgres 18 |
localhost:6379 |
Redis 7 |
localhost:1030 |
Mailpit SMTP — FORWARD_MAILPIT_PORT en backend/.env.docker |
Estos puertos son los configurados en
backend/.env.docker. Si el archivo no existe, compose cae a sus defaults (8080,8025,1025).
./stack.sh ps # todos los servicios Up (postgres healthy)
curl -I http://localhost:8081 # Laravel responde
curl -I http://localhost:3000 # Next.js dev responde
./stack.sh logs --backend | grep -i migrat # migraciones aplicadas
./stack.sh shell app --backend # y adentro: php artisan migrate:statusEl primer
updel frontend puede tardar 1–2 min poblando el volumen anónimonode_modules(pnpm install --frozen-lockfile). Editá algo enapp-next/para comprobar el hot-reload en:3000. Mailpit UI en http://localhost:8027.
stack.sh envuelve los dos docker compose del proyecto detrás de un único punto de entrada. Se invoca desde la raíz del repo.
./stack.sh <comando> [opciones] [-- args extra para docker compose]
| Comando | Acción |
|---|---|
build |
Construye las imágenes |
up |
Levanta los contenedores (detached por defecto) |
down |
Baja y elimina los contenedores (--fresh también borra volúmenes) |
restart |
Reinicia/recarga contenedores ya creados |
rebuild |
down + build + up (reset, preserva volúmenes) |
logs |
Sigue los logs (-f --tail=100) |
ps / status |
Muestra estado de contenedores |
shell <svc> |
Abre sh dentro de un servicio (requiere --backend o --frontend) |
help |
Muestra la ayuda |
| Opción | Efecto |
|---|---|
--backend |
Aplica solo al backend |
--frontend |
Aplica solo al frontend |
--prod |
Frontend en modo producción (default: dev con hot-reload) |
--workers |
Backend: agrega perfil workers (queue + scheduler) |
--dev-assets |
Backend: agrega perfil dev-assets (Vite en :5173) |
--no-detach |
up en foreground (sin -d) |
--fresh |
En down: borra volúmenes (-v). Pierde datos de Postgres. |
-h, --help |
Ayuda |
-- <args> |
Todo lo posterior se reenvía tal cual a docker compose |
Sin
--backend/--frontend, los comandos aplican a las dos mitades.
# Levantar todo (backend + frontend dev)
./stack.sh up
# Solo frontend, modo producción
./stack.sh up --frontend --prod
# Backend + workers (queue + scheduler)
./stack.sh up --backend --workers
# Reconstruir frontend tras cambiar Dockerfile o package.json
./stack.sh build --frontend
./stack.sh up --frontend
# Ver logs en vivo de toda la stack
./stack.sh logs
# Logs solo del frontend
./stack.sh logs --frontend
# Estado de los contenedores
./stack.sh ps
# Reset completo del frontend
./stack.sh rebuild --frontend
# Bajar todo
./stack.sh down
# Bajar y borrar volúmenes (Postgres + Redis quedan vacíos)
./stack.sh down --fresh
# Shell en el contenedor Laravel
./stack.sh shell app --backend
# Shell en el contenedor Next
./stack.sh shell frontend --frontend
# Passthrough: agregar --remove-orphans al up
./stack.sh up -- --remove-orphansstack.sh |
Equivalente manual |
|---|---|
./stack.sh up --backend |
cd backend && docker compose --env-file .env.docker -f docker-compose.dev.yml up -d |
./stack.sh up --backend --workers |
cd backend && docker compose --env-file .env.docker -f docker-compose.dev.yml --profile workers up -d |
./stack.sh up --frontend (dev) |
cd frontend && docker compose --env-file docker/.env -f docker/compose/docker-compose.dev.yml up -d |
./stack.sh up --frontend --prod |
cd frontend && docker compose --env-file docker/.env -f docker/compose/docker-compose.yml up -d |
Ambas mitades usan --env-file distinto y el script ya lo incluye — no hay que recordarlo:
- Backend →
--env-file .env.docker(puertos del host, UID/GID, credenciales de Postgres). - Frontend →
--env-file docker/.env(incluyeNEXT_PUBLIC_API_URL,NEXT_VERSION).
stack.sh orquesta docker compose, pero el flujo de trabajo de cada lado tiene comandos propios. Para esos, entrá al contenedor con shell o usá compose exec directamente.
# Tests
./stack.sh shell app --backend
php artisan test
php artisan test tests/Architecture/ # gates de arquitectura (deben pasar)
./vendor/bin/pint # estilo (obligatorio antes de commit)
# Artisan / Composer
php artisan migrate
php artisan route:list
composer require vendor/packageMás detalle en backend/README.md y backend/app-laravel/CLAUDE.md.
# Si preferís correr Node en el host (sin Docker):
cd frontend/app-next
pnpm install
pnpm dev # :3000
pnpm build
pnpm lint
# O dentro del contenedor:
./stack.sh shell frontend --frontend
pnpm installMás detalle en frontend/INIT.md y frontend/app-next/CLAUDE.md.
.envapuntando a hosts externos de Ray/Telescope (192.168.200.241): siphp artisanse cuelga enpoll_schedule_timeout, comentáRAY_HOST,VAR_DUMPER_SERVERy ponéTELESCOPE_ENABLED=falseenbackend/app-laravel/.env. (Aplica solo si tu app trae esa config; la instalación vanilla no.)entrypoint.shredirige stdin con</dev/nullintencionalmente: sin eso,storage:linkcuelga el arranque de php-fpm.
| Síntoma | Causa probable | Solución |
|---|---|---|
backend/.env.docker no existe al correr stack.sh ... --backend |
Falta el archivo con puertos/UID del compose | Crear backend/.env.docker con las vars de la sección "Variables del compose" del backend |
frontend/docker/.env no existe al correr stack.sh up --frontend |
Falta el archivo de env del frontend | cp frontend/docker/.env.example frontend/docker/.env y editar |
next build falla por env faltante |
Falta NEXT_PUBLIC_API_URL |
Verificar frontend/docker/.env y ./stack.sh build --frontend |
port is already allocated al levantar mailpit (1025/8025) |
Otro stack ya bindeó esos puertos | Editar FORWARD_MAILPIT_PORT / FORWARD_MAILPIT_DASHBOARD_PORT en backend/.env.docker |
| Puerto 8081 ocupado | Otro servicio lo usa | Cambiar APP_PORT en backend/.env.docker |
| Puerto 80 ocupado (frontend prod) | Otro servicio en :80 |
Editar frontend/docker/compose/docker-compose.yml ("80:80" → "8081:80") |
Connection refused desde frontend al backend |
Laravel escucha en 127.0.0.1 en lugar de 0.0.0.0 |
El stack del backend ya lo expone en 0.0.0.0:${APP_PORT}; verificar con ./stack.sh ps |
| Schemas de Postgres faltan | El init solo corre con volumen vacío | ./stack.sh down --fresh && ./stack.sh up |
Cambié NEXT_PUBLIC_* y no toma efecto |
Esas variables se inlinean en build time | ./stack.sh rebuild --frontend |
Permisos en backend/app-laravel/storage/ |
El contenedor corre como UID 1000 | Ajustar USER_ID/GROUP_ID en backend/.env.docker y ./stack.sh rebuild --backend |
Postgres no arranca tras subir a la v18 (database files are incompatible) |
El volumen pgdata tiene datos de Postgres 17; PG18 no los lee |
Backup si hace falta y recrear el volumen: ./stack.sh down --fresh --backend && ./stack.sh up --backend |
El repositorio se aloja en Gitea y sigue una estrategia tipo Git Flow simplificada. Las ramas principales son permanentes; las de apoyo nacen, se mergean y se eliminan.
| Rama | Rol | Reglas |
|---|---|---|
main |
Producción | Cada commit es un tag de versión (v1.0.1, …). Nunca push directo. Solo recibe merges desde staging o hotfix/*. |
staging |
Réplica de producción para QA | Entorno idéntico al real. Aquí se valida (Cálculo de Ganancias, Asistencia, etc.) antes de un release. Recibe merges desde dev. |
dev |
Integración diaria | Rama de trabajo donde se consolidan features de backend y frontend antes de pasar a staging. |
Nacen de dev o main y se eliminan al mergearse.
| Rama | Nace de | Mergea hacia | Uso |
|---|---|---|---|
feature/<slug> |
dev |
dev |
Nuevas funcionalidades |
hotfix/<slug> |
main |
main y dev |
Parches urgentes en producción |
bugfix/<slug> |
dev |
dev |
Corrección de errores pre-producción |
docs/<slug> |
dev |
dev |
Cambios exclusivos de documentación |
Ejemplo: feature/attendance-biometric. Cada desarrollador trabaja aislado para no romper dev. Al terminar, Pull Request → dev.
Única rama que puede nacer de main y saltarse el flujo normal cuando hay un error crítico (ej. un fallo en el Withholding Engine Service que afecta nómina). Una vez corregido, se mergea tanto a main como a dev para que la corrección no se pierda en el siguiente release.
Igual que feature/, pero enfocada en arreglar fallos que aún no llegaron a producción.
Cambios solo en archivos como README.md, CLAUDE.md o manuales — evita mezclar texto con commits de código.
feature/ ──→ dev ──→ staging ──→ main
↑
hotfix/ ─┘
- Crear
feature/<slug>desdedev. - PR a
devpara integrar con el equipo. - Promover
dev → stagingpara QA / cliente. - Promover
staging → mainpara el release oficial (tag). - Si surge un crítico en producción:
hotfix/<slug>desdemain→ merge amainydev.
Sigue el formato definido en backend/.claude/rules/laravel-conventions.md:
[Module] type: brief description
Tipos: feat | fix | refactor | test | docs | chore | perf
Ejemplos:
[HR] feat: add leave request approval workflow
[Attendance] fix: correct overlap detection in working hours
[Architecture] refactor: unify exception hierarchy under DomainException
Si el commit cierra un ticket de Taiga, agregá la referencia al final:
[HR] feat: add leave request approval workflow
Closes RRHH-123
backend/README.md— uso detallado del stack backend, perfiles, build de producción, schemasbackend/app-laravel/CLAUDE.md— arquitectura por capas, módulos, convencionesbackend/.claude/rules/— gates obligatorios (architecture-boundaries, hardening, testing-standards, conventions)frontend/app-next/CLAUDE.md— patrón Repository/Service/DTO, Server Actions, NextAuthCLAUDE.md(raíz) — mapa para Claude Code