Arquitectura: visión general
Cómo se organiza el código de platform (un solo repo, vertical slicing por dominio, props siempre vía Data object) y qué se difiere con disparador.
La arquitectura de código quedó cerrada en una sesión de diseño (decisión 0.7) y
el foundation ya la está fijando en código: F0–F2 mergeadas (bootstrap, auth,
tenancy + RLS). Los slices de dominio (app/Domain/*) llegan con las fases de
producto; hasta entonces, lo de abajo describe la convención que el código nuevo
sigue.
El principio que gobierna toda la arquitectura, en palabras del CTO:
Diseñar para el futuro (no cerrarse puertas) es barato; construir para el futuro es sobre-ingeniería.
La estructura queda flexible por diseño y liviana por recorte. Lo especulativo (una API REST, interfaces entre slices) se difiere con un disparador concreto, no se construye “por si acaso”.
El mapa
Así fluye un request por platform, de arriba hacia abajo. Las secciones que
siguen explican cada frontera del diagrama.
Navegador — React + Inertia
TypeScript estricto, shadcn
platform (un solo repo Laravel)
Rutas tenant
el producto, con middleware de tenancy
Rutas centrales · /admin/*
backoffice legal, sin tenancy (cross-tenant)
Controllers delgados → Actions
hidratan Data, invocan la lógica, renderean
Domain/Labor
motor del Código de Trabajo — PHP puro, cero deps
Domain/Contracts
aggregate Contract, eventos, documento
Domain/Signing
frontera SigningService (UANATACA / fake)
Models (Eloquent)
solo persistencia
Postgres — tablas de dominio
tenant_id + RLS fail-closed
Postgres — tablas globales
users, tenants, clauses (sin tenant_id)
platform. Las dos fronteras duras: los props a React pasan siempre por un Data object, yDomain/Labor no conoce Laravel.Un solo repo
platform = el producto (app) más el backoffice legal interno, en un mismo
repo Laravel. Comparten el dominio: el backoffice edita las cláusulas y el motor
que el producto consume, así que separarlos duplicaría o acoplaría por red el
activo crítico.
Producto + backoffice legal juntos. Es el patrón que stancl/tenancy endosa:
rutas centrales (sin middleware de tenancy) para el backoffice
cross-tenant, rutas tenant (con middleware) para el producto.
La tool interna del equipo (clon de Basecamp) vive en su propio repo: cero dominio compartido con el producto, ya existe.
El backoffice legal vive en /admin/* del mismo platform, sobre las tablas
globales/centrales (sin tenant_id), no como un deploy aparte. Se construye en
Inertia + React + shadcn (el mismo stack del producto), no en Filament: mantener
un solo paradigma de frontend baja la carga de review y reusa el design system.
El detalle de cómo se aísla cada cliente —el modelo de tenancy, el tenant_id y
las garantías de aislamiento— vive en Multi-tenancy y aislamiento.
Vertical slicing por dominio
app se organiza por dominio, no por capa técnica. La estructura “grita” que
es una legal-tech de contratos.
Cada slice lleva sus Actions, Models, Data y Policies. Reusa las convenciones
de AxelOps (Actions sin sufijo, spatie/laravel-data, Pest, phpstan nivel 7) pero
no su estructura: AxelOps organiza por capa porque es una tool interna pequeña;
platform es un producto complejo donde el vertical slicing paga. Es disciplina de
carpetas, no ceremonia: nada de nwidart/laravel-modules.
El slice más puro es app/Domain/Labor/: PHP puro, cero dependencias de
Laravel, Eloquent o HTTP. Es la única pieza que merece esa frontera, justo porque
el resto puede ser CRUD normal. Ver Motor del Código de Trabajo.
Convenciones de código
La frase que lo gobierna: “Inertia no te encierra; los controllers gordos sí.” Con la lógica en Actions, la puerta a mobile y a MCP queda abierta sin construir nada hoy.
Larastan en nivel 7, Pint para formato, Pest para tests, TypeScript estricto en el front desde el día 1 (el PoC no tenía TS: no se hereda esa carencia).
Props siempre vía Data object
La regla que cierra la fuga de PII y cross-tenant: los datos que bajan a React
nunca son un modelo Eloquent crudo. Siempre pasan por un objeto
spatie/laravel-data que declara explícitamente qué campos salen (whitelist).
Un campo sensible (salario, DUI) solo llega al cliente si el DTO lo incluye
deliberadamente. Bonus: el mismo Data object sirve props de Inertia y JSON de
una API futura, una sola capa de serialización. Los errores de validación usan el
mecanismo estándar de Inertia (errors en props), sin envolver.
Qué se difiere (con disparador)
Los dos escenarios futuros que se marcaron (front alternativo/mobile, extraer un módulo a servicio) no requieren construir nada hoy. Requieren solo no meter la lógica en el lugar equivocado.
| Diferido | Disparador para construirlo |
|---|---|
api.php + Sanctum (capa API para mobile) | Existe un 2º cliente real (mobile/integración) |
| Interfaces + eventos entre slices, sin FK cross-módulo | Decisión concreta de extraer un slice (ej. Signing) a un servicio |
nwidart/laravel-modules | 8-10 dominios que colisionen (carpetas + disciplina bastan antes) |
Motor a paquete Composer (packages/labor-engine) | Entra la 2ª jurisdicción y se quiere versionar el motor aparte del deploy |
Por qué importaba cerrarlo antes de escribir código
Sin la arquitectura decidida, el primer commit habría fijado la estructura sin una decisión consciente. Ahora está resuelta: flexible por diseño, liviana por recorte. Toda la base de código de #1 en adelante se escribe sobre esto.