Saltar a contenido

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 de brand.config.ts. El nombre de la propia plataforma se lee de brand.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 del SSO_ISSUER del 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ó excluido public/languages y 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 el globals.css de 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 los className del 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 ❌ 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