Código compartido¶
Las plataformas del ecosistema son distintas por fuera y una sola por dentro. Para el usuario cada una tiene su marca, su dominio y sus servicios, pero más del 90% del código procede de una única fuente.
Esta página explica cómo se distribuye ese código compartido.
Por qué hizo falta¶
Al principio cada plataforma se copiaba de la plantilla. El resultado era que una sola corrección debía repetirse a mano en 8 repositorios: si se olvidaba uno, esa plataforma se quedaba atrás en silencio.
La medición mostró que ~95% del frontend era realmente compartido; la divergencia real eran unas 40 líneas con el nombre de la marca. Es decir, la duplicación no era un requisito técnico sino una herencia de la copia.
Tres mecanismos¶
El código compartido viaja de forma distinta según la capa. Ninguno es «copiar y pegar»: todos están versionados y son reversibles.
| Capa | Forma | Mecanismo |
|---|---|---|
| Núcleo del backend | módulo Go | dependencia en go.mod |
| Capa de frontend | paquete npm | dependencia en package.json |
| Esqueleto de la plataforma | historia git | git merge + autosync diario |
1. Núcleo del backend — un módulo Go¶
Autenticación, control de acceso por roles (RBAC), API gateway, auditoría,
canalización de IA, integración eID/SSO: todo ello vive en un solo módulo
Go. El main.go de una plataforma suele tener unas 30 líneas: arrancar el
núcleo y añadir las rutas propias de esa plataforma.
El núcleo tiene una sola capa directa:
open-gerege-core— la base abierta que consumen directamente todos los backends gubernamentales y de Gerege.
Hasta el 2026-08-02, private-gerege-core se situaba en medio. Como no contenía
lógica adicional ni migraciones, se retiró de la cadena y se archivó. La lógica
comercial permanece en el repositorio de cada producto.
2. Capa de frontend — @gerege/ui-core¶
El mismo problema que el núcleo resolvió en el backend, resuelto de nuevo en el frontend. El paquete contiene:
lib/**— cliente de API, utilidades BFF, diccionario i18n, tema, sesión,components/**— armazón, administración, área de usuario, eID, gateway,api/**— la lógica de 158 rutas BFF.
El paquete se publica como código fuente TypeScript (sin compilar), de modo
que la aplicación lo compila mediante transpilePackages de Next.js. La
distribución es un tarball HTTPS abierto: no requiere autenticación y
funciona dentro de una compilación de Docker.
Por qué las rutas BFF conservan un envoltorio
Next.js registra las rutas mediante el sistema de archivos, así que cada aplicación mantiene una reexportación de una línea por ruta:
// src/app/api/org/[id]/route.ts
export { GET, PUT, DELETE } from '@gerege/ui-core/api/org/[id]';
export const dynamic = 'force-dynamic';
Los 158 archivos podrían reducirse a un único [...path], pero eso
destruiría una lista de permitidos de seguridad: el listado de rutas
define a qué caminos del backend puede llegar el navegador. El envoltorio
es un precio deliberado.
3. Esqueleto de la plataforma — herencia git¶
Lo que no pertenece al paquete (estructura de páginas, globals.css,
configuración de despliegue) se hereda de la plantilla mediante git merge.
Un autosync diario trae los cambios de la plantilla superior y abre un pull
request en los repositorios de las aplicaciones: todo cambio que llega a
producción pasa por revisión humana.
Los archivos que deben seguir siendo propios de cada plataforma (marca,
despliegue, CI, documentación) están protegidos con merge=ours en
.gitattributes.
merge=ours no protege frente a cambios unilaterales
Ese driver solo resuelve conflictos. Si la plantilla superior elimina un archivo, la fusión lo sigue: el driver no llega a invocarse. La protección real es que cada archivo de marca o configuración tenga contenido distinto en ambos lados.
Qué sigue siendo propiedad de la plataforma¶
| En el paquete / núcleo | Propiedad de la plataforma |
|---|---|
lib/**, components/**, lógica BFF |
brand.config.ts — nombre, dominio, colores, URL de documentación |
| Autenticación, RBAC, gateway, auditoría | components/landing/** — texto de marketing |
| Integración eID / SSO | app/**/page.tsx — registro de rutas (envoltorios finos) |
| Diccionario i18n compartido (846 claves × 7 idiomas) | lib/<platform>I18n.ts — terminología de la plataforma |
Estructura del menú (AppShell) |
nav.config.ts — qué secciones presta la plataforma |
app/globals.css — tokens de color de marca |
|
deploy/**, .github/** — despliegue, CI |
Por qué la terminología de plataforma se queda en la aplicación¶
La regla: el diccionario compartido solo conoce la superficie compartida. Las palabras que pertenecen a una sola plataforma —el vocabulario de IBAN y extractos del monedero, el catálogo de API del portal de desarrolladores, la terminología de procesos de negocio de Ring— residen en el diccionario propio de la aplicación.
El motivo es el coste: meter los 1,104 términos de Ring en el diccionario compartido hace que kiosk, POS y el monedero carguen con ellos, y con cada idioma añadido ese coste se multiplica por siete.
La implementación sigue el mismo patrón en todos los repositorios:
// lib/walletI18n.ts — los 15 términos del monedero × 4 idiomas
export function useWalletT() { … } // recurre al inglés en los idiomas sin traducir
Si un componente pasa T como prop a sus componentes hijos, dividirlo en
dos funciones (T + wt) obligaría a dividir también cada prop. En ese caso se
escribe un único resolutor: si la clave es de la plataforma se toma de su propio
diccionario y, si no, del paquete (lib/lang.ts de ring-dgov).
Menú — estructura compartida, servicio propio de la plataforma¶
AppShell tiene la misma organización en todas las plataformas (Superadmin ·
Admin · Gestor · Ciudadano), pero cada plataforma implementa solo un
subconjunto de ella: el monedero no tiene módulos de gateway, relay ni
registros.
| Ajuste | Función |
|---|---|
navRoutes |
Las rutas que la aplicación presta de verdad; el menú se filtra por ellas |
navSystemLabels |
Nombres de sistema en el rail (me → «Monedero») |
navExtra |
Entradas de menú que solo existen en esa plataforma (las 21 entradas BPM de Ring) |
navExtra llega desde un componente CLIENT
UiCoreProvider es un componente client al que se llama desde el root
layout del servidor. Los iconos del menú (componentes de React) y las
funciones de etiqueta no cruzan la frontera server→client. Por eso la
aplicación crea un envoltorio client fino y los pasa desde dentro de él:
// src/nav.config.tsx
'use client';
export default function AppNav({ children }) {
return <UiCoreProvider navExtra={NAV_EXTRA}>{children}</UiCoreProvider>;
}
Si una entrada de menú que solo prestan unas pocas plataformas se marca en
el paquete con optIn: true, aparecerá únicamente en las plataformas que la
hayan escrito explícitamente en navRoutes.
Tres barreras automáticas¶
El código compartido genera tres tipos distintos de dependencia. Cada uno se manifiesta de otra forma al romperse, así que las barreras también son tres:
| Dependencia | Barrera | Qué ocurre al romperse |
|---|---|---|
| Código del paquete ← código de la aplicación | tsc |
La compilación falla: se ve al instante |
| Ruta del paquete ← envoltorio BFF de la aplicación | check-routes |
El endpoint desaparece en silencio |
| Clase del paquete ← CSS de la aplicación | check-styles |
La pantalla pierde el estilo en silencio |
-
check-brand— la compilación falla si el nombre de una plataforma aparece en el código fuera debrand.config.ts. El nombre de la propia plataforma se lee debrand.config.ts, así que la lista no se queda obsoleta a mano.Qué atrapó la barrera
La página de inicio de sesión ofrecía entrar por «Gerege SSO (sso.gerege.mn)» cuando las plataformas de la línea gubernamental redirigían en realidad a
sso.dgov.mn. Ahora el host se lee delSSO_ISSUERdel backend. -
check-routes— exige un envoltorio en la aplicación para cada ruta del paquete. Sin él, un endpoint nuevo del paquete desaparecería en silencio en esa plataforma (la lógica no se ve dentro de la aplicación, así que nada parece roto).La barrera no demuestra que EXCLUDE sea correcto
Una ruta que a propósito no se expone se anota en
EXCLUDE: la divergencia queda explícita. Pero la barrera no atrapa una entrada mal escrita. Así fue como en una plataforma quedó excluidopublic/languagesy el selector de idioma se quedó vacío: todas las barreras en verde y la pantalla rota. -
check-styles— el paquete no contiene CSS: el estilo vive en elglobals.cssde cada repositorio. Cuando el paquete nombra una clase nueva, o el CSS del repositorio se queda anticuado, el componente pierde en silencio su estilo: el botón se queda con el gris por defecto del navegador y la tabla sin bordes. Esta barrera coteja losclassNamedel paquete con el CSS del repositorio.
Versionado¶
Los tres mecanismos siguen semver. Al publicarse una versión nueva,
Dependabot abre un pull request en los repositorios consumidores; la
actualización en sí es un cambio de una línea en go.mod o package.json.
Subir la versión de la plantilla no basta¶
Como cada plataforma hereda de la plantilla, es tentador pensar que «subiendo la plantilla se propaga a todas». En realidad los dos tipos de dependencia funcionan de forma distinta:
| Archivo | merge=ours? |
¿Se propaga desde la plantilla? |
|---|---|---|
backend/go.mod |
sí | ❌ nunca |
frontend/package.json |
no | ✅ sí |
La protección de go.mod es un requisito estructural: la línea module es
distinta en cada repositorio (…/gerege-app-mn/backend frente a
…/wallet-gerege-mn/backend), de modo que cada fusión chocaría ya en la primera
línea. Por eso subir en la plantilla la versión del núcleo del backend no
propaga nada: hace falta un pull request en cada repositorio.
El árbol de herencia tiene además tres niveles (public template → private
template → aplicación), así que incluso un archivo capaz de propagarse tarda
varios ciclos de autosync en llegar a las hojas.
Un cambio rompedor atasca los pull requests de dependencias
Ampliar el diccionario de cuatro idiomas a siete rompió todos los puntos
donde figuraba Record<Lang, …>. El resultado fue que todos los pull
requests de Dependabot fallaban en tsc, nadie los fusionaba y el siguiente
se amontonaba encima: la flota se dispersó de la v0.4.0 a la v0.10.2.
Por eso, al hacer un cambio rompedor en un paquete: (a) anote las instrucciones de migración en las notas de la versión y (b) publique la corrección en los repositorios consumidores a la vez. La actualización automática necesita un paso manual ante los cambios rompedores.
Quedarse atrás es silencioso
Si los pull requests de dependencias se acumulan, las plataformas se dispersan entre versiones distintas y se rompe la promesa de que «una corrección llega a todos». Cerrar esos pull requests con regularidad es una condición de funcionamiento de esta estructura, no un extra opcional.
Relacionado¶
- Pila tecnológica
- Convenciones de plataforma
- Autenticación y autorización — el ajuste
AUTH_MODE