Todos los artículos

Más allá del software: construyendo nautaran, una plataforma GIS para infraestructura municipal

Publicado 17 jul 20266 min de lectura
  • Producto
  • Ingeniería
  • SIG

Ideas clave

  • Nautaran es el primer producto B2G de Maladeta: una plataforma GIS multi-tenant para que los ayuntamientos gestionen las redes municipales de agua, saneamiento, pluviales y alumbrado.
  • La plataforma se construyó multi-tenant desde el primer día — nada da por fijo al primer tenant, el Ajuntament de Naut Aran — con módulos opcionales activables por tenant.
  • El aislamiento entre tenants se aplica con row-level security de PostgreSQL usando un patrón fail-closed con current_setting(..., true), no solo con un scope a nivel de ORM.

Todos los productos que Maladeta Studio ha lanzado hasta ahora tienen algo en común: un usuario final que eligió estar ahí. Los huéspedes de Lueira reservan una clase de esquí porque quieren esquiar. Los senderistas de MeteoTrail consultan una previsión porque están planeando una ruta. Incluso la plataforma de marketplace, cualquiera que sea la forma que tome para un cliente dado, existe porque alguien quería comprar o vender algo. Nautaran rompe ese patrón. Nadie se apuntó a esto — lo heredaron en forma de un archivador.

Y eso no es una metáfora. Entre 2017 y 2018, alguien levantó las redes de agua, saneamiento, pluviales y alumbrado público de un municipio del Val d'Aran: 9.902 elementos geográficos, recorridos y cartografiados a mano. Y luego se quedó ahí. Sin mantenimiento, sin actualizaciones, alejándose poco a poco de lo que realmente hay enterrado. Ese es el punto de partida de nautaran (nombre interno: Urban), una nueva línea de producto B2G para el estudio — software construido para ayuntamientos, no para sus vecinos.

Es un tipo de proyecto distinto de todo lo que hemos construido antes, y quiero escribir sobre las decisiones que hicieron que se sintiera como una línea de producto real en lugar de un trabajo GIS puntual para un cliente.

1. De escuelas de esquí a ayuntamientos: un tipo de cliente nuevo

Los instintos de tecnología cívica del estudio no son nuevos — el trabajo de NaturPark y los posts sobre aparcamiento y residuos de hace un tiempo ya apuntaban en esta dirección. Nautaran es la primera vez que convertimos ese instinto en un producto formal y vendible en lugar de un proyecto puntual.

El cliente es un ayuntamiento, y el "usuario" dentro de él se divide en al menos dos personas: alguien de oficina que necesita saber dónde está una válvula antes de firmar un permiso de obra, y una brigada — el equipo de mantenimiento — que necesita la misma información de pie en una zanja, con guantes mojados y sin cobertura. Diseñar para ambos desde el primer día marcó casi cada decisión técnica que vino después, empezando por el hecho de que esto no podía ser una única aplicación.

2. Una plataforma base más módulos, no funcionalidades fijadas a un solo tenant

Naut Aran es nuestro primer tenant, pero nada en el código asume que sea el único. Esa es una lección que ya aprendimos por las malas con el propio trabajo de multi-tenancy de Lueira — reconvertir en aislado algo escrito tenant a tenant duele más que hacerlo bien desde el principio. Así que nautaran es multi-tenant desde la primera migración: autenticación, control de acceso basado en roles, registro de auditoría, almacenamiento de archivos, un modelo de datos de capa/tipo/elemento, servido de teselas de mapa, un backoffice de tenant y una consola interna de Maladeta se envían todos como una única plataforma base, vendida como un bloque a cada ayuntamiento que se incorpora.

Sobre esa base se asientan módulos, activables por tenant. Hoy existen dos. serveis_publics es el "visor" — una vista combinada de mapa y tabla para navegar el inventario de las redes de agua, saneamiento, pluviales y alumbrado. app_camp es una Progressive Web App con capacidad offline construida para la brigada: tiene que funcionar cuando el equipo está de pie junto a una tapa de alcantarilla sin conectividad, así que el modelo de datos y la lógica de sincronización se diseñaron desde el principio en torno a la consistencia eventual.

Un módulo se puede desactivar para un tenant sin tocar sus datos. Desactivar app_camp para un ayuntamiento que no está listo para él simplemente hace que sus rutas, teselas y entradas de navegación dejen de resolverse — no se borra nada. Es la misma idea de módulos activables de NaturPark, aplicada a algo con implicaciones operativas reales: un ayuntamiento puede pilotar un módulo y volver a desactivarlo si no le encaja, sin arriesgar nunca los datos del levantamiento.

3. Por qué row-level security en lugar de solo un scope del ORM

El aislamiento multi-tenant de Lueira se apoya en un scope por defecto a nivel de ORM con una restricción de base de datos como respaldo — razonable para una plataforma donde la peor fuga posible es que alguien vea las reservas de clases de esquí de otra escuela. El peor caso de nautaran es mucho peor: un mapa filtrado de la red de agua o saneamiento del municipio equivocado es información de infraestructura genuinamente sensible, no una vergüenza. Esa diferencia de nivel de riesgo es la razón por la que me apoyé en el row-level security nativo de PostgreSQL como mecanismo de aplicación principal, en lugar de como una segunda línea de defensa.

El detalle del que estoy más orgulloso es cómo se lee el contexto del tenant. Las políticas de RLS extraen el tenant actual de una variable de sesión a través de current_setting(..., true) — la forma con dos argumentos, que devuelve NULL en lugar de lanzar un error cuando la variable no está definida:

ALTER TABLE network_elements ENABLE ROW LEVEL SECURITY;
ALTER TABLE network_elements FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON network_elements
    USING (
        tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid
    );

Si una petición llega de alguna manera a la base de datos sin haber fijado app.tenant_id — un bug en el middleware, un SET LOCAL olvidado, un job en segundo plano que se saltó la configuración de contexto — esa comparación se evalúa contra NULL, y tenant_id = NULL nunca es verdadero para ninguna fila. La consulta devuelve cero filas. No falla con un error, y tampoco devuelve accidentalmente los datos de todos los tenants, que es el modo de fallo que obtendrías con un fallback ingenuo de "si no hay tenant, muestra todo". Cierra en falso por construcción, aplicado a nivel de base de datos sin importar por cuál de los tres frontends en Next.js o por qué ruta de código del backend haya entrado la petición.

El servidor de teselas recibe el mismo tratamiento desde otro ángulo. Martin sirve teselas vectoriales MVT directamente desde PostGIS, y las URLs de MVT pueden parecer recursos estáticos inofensivos si no prestas atención — el tipo de cosa que es fácil dejar accesible al mundo por accidente. Pusimos Caddy delante haciendo forward_auth, así que el propio servidor de teselas no tiene ningún acceso público sin autenticar.

4. Lo que está diseñado pero aún no construido

El modelo de datos ya tiene hueco para módulos que todavía no existen: catastro (registro de la propiedad), padró (el registro municipal de residentes), activitats_economiques (el registro de actividades económicas locales) y 3D tiles. Ninguno está construido. Diseñar el esquema para ellos ahora, en lugar de descubrir más adelante que la base no puede encajar un módulo de padró sin cirugía, es la misma apuesta que la multi-tenancy: pagar el precio de la generalidad pronto, mientras es barato, en las partes de las que estás razonablemente seguro de que las vas a necesitar.

Cierre

Nautaran no está publicado, no está probado a escala, y tiene exactamente un tenant por ahora. Pero ese tenant es un ayuntamiento real cuyos 9.902 elementos llevaban sin tocarse la mayor parte de una década, y devolver esos datos a un sistema que la gente realmente usa ya ha justificado el trabajo de la plataforma. Que el resto de los módulos lleguen a construirse depende de si más ayuntamientos quieren sumarse — pero la base está construida para que puedan hacerlo, sin que tengamos que reescribir nada para permitírselo.