Tots els articles

Més enllà del programari: construint nautaran, una plataforma GIS per a infraestructura municipal

Publicat 17 de jul. del 20266 min de lectura
  • Producte
  • Enginyeria
  • SIG

Idees clau

  • Nautaran és el primer producte B2G de Maladeta: una plataforma GIS multi-tenant perquè els ajuntaments gestionin les xarxes municipals d'aigua, sanejament, pluvials i enllumenat.
  • La plataforma es va construir multi-tenant des del primer dia — res dona per fix el primer tenant, l'Ajuntament de Naut Aran — amb mòduls opcionals activables per tenant.
  • L'aïllament entre tenants s'aplica amb row-level security de PostgreSQL fent servir un patró fail-closed amb current_setting(..., true), no només amb un scope a nivell d'ORM.

Tots els productes que Maladeta Studio ha llançat fins ara tenen una cosa en comú: un usuari final que va triar ser-hi. Els hostes de Lueira reserven una classe d'esquí perquè volen esquiar. Els senderistes de MeteoTrail consulten una previsió perquè estan planificant una ruta. Fins i tot la plataforma de marketplace, sigui quina sigui la forma que prengui per a un client donat, existeix perquè algú volia comprar o vendre alguna cosa. Nautaran trenca aquest patró. Ningú s'hi va apuntar — ho van heretar en forma d'un arxivador.

I això no és una metàfora. Entre el 2017 i el 2018, algú va aixecar les xarxes d'aigua, sanejament, pluvials i enllumenat públic d'un municipi de la Val d'Aran: 9.902 elements geogràfics, recorreguts i cartografiats a mà. I després es va quedar allà. Sense manteniment, sense actualitzacions, allunyant-se a poc a poc del que realment hi ha soterrat. Aquest és el punt de partida de nautaran (nom intern: Urban), una nova línia de producte B2G per a l'estudi — programari construït per a ajuntaments, no per als seus veïns.

És un tipus de projecte diferent de tot el que hem construït abans, i vull escriure sobre les decisions que van fer que se sentís com una línia de producte real en lloc d'una feina GIS puntual per a un client.

1. D'escoles d'esquí a ajuntaments: un tipus de client nou

Els instints de tecnologia cívica de l'estudi no són nous — la feina de NaturPark i els posts sobre aparcament i residus d'un temps enrere ja apuntaven en aquesta direcció. Nautaran és la primera vegada que convertim aquest instint en un producte formal i venible en lloc d'un projecte puntual.

El client és un ajuntament, i l'"usuari" dins seu es divideix en almenys dues persones: algú d'oficina que necessita saber on és una vàlvula abans de signar un permís d'obra, i una brigada — l'equip de manteniment — que necessita la mateixa informació dreta en una rasa, amb guants mullats i sense cobertura. Dissenyar per a tots dos des del primer dia va marcar gairebé cada decisió tècnica que va venir després, començant pel fet que això no podia ser una única aplicació.

2. Una plataforma base més mòduls, no funcionalitats fixades a un sol tenant

Naut Aran és el nostre primer tenant, però res al codi assumeix que sigui l'únic. Aquesta és una lliçó que ja vam aprendre per les males amb la mateixa feina de multi-tenancy de Lueira — reconvertir en aïllat alguna cosa escrita tenant a tenant fa més mal que fer-ho bé des del principi. Així que nautaran és multi-tenant des de la primera migració: autenticació, control d'accés basat en rols, registre d'auditoria, emmagatzematge de fitxers, un model de dades de capa/tipus/element, servei de teseŀles de mapa, un backoffice de tenant i una consola interna de Maladeta s'envien tots com una única plataforma base, venuda com un bloc a cada ajuntament que s'incorpora.

Sobre aquesta base s'asseuen mòduls, activables per tenant. Avui n'hi ha dos. serveis_publics és el "visor" — una vista combinada de mapa i taula per navegar l'inventari de les xarxes d'aigua, sanejament, pluvials i enllumenat. app_camp és una Progressive Web App amb capacitat offline construïda per a la brigada: ha de funcionar quan l'equip és dret al costat d'una tapa de clavegueram sense connectivitat, així que el model de dades i la lògica de sincronització es van dissenyar des del principi al voltant de la consistència eventual.

Un mòdul es pot desactivar per a un tenant sense tocar-ne les dades. Desactivar app_camp per a un ajuntament que no hi està preparat simplement fa que les seves rutes, teseŀles i entrades de navegació deixin de resoldre's — no s'esborra res. És la mateixa idea de mòduls activables de NaturPark, aplicada a alguna cosa amb implicacions operatives reals: un ajuntament pot pilotar un mòdul i tornar-lo a desactivar si no li encaixa, sense arriscar mai les dades de l'aixecament.

3. Per què row-level security en lloc de només un scope de l'ORM

L'aïllament multi-tenant de Lueira es recolza en un scope per defecte a nivell d'ORM amb una restricció de base de dades com a suport — raonable per a una plataforma on la pitjor fuga possible és que algú vegi les reserves de classes d'esquí d'una altra escola. El pitjor cas de nautaran és molt pitjor: un mapa filtrat de la xarxa d'aigua o sanejament del municipi equivocat és informació d'infraestructura genuïnament sensible, no una vergonya. Aquesta diferència de nivell de risc és la raó per la qual em vaig recolzar en el row-level security natiu de PostgreSQL com a mecanisme d'aplicació principal, en lloc de com a segona línia de defensa.

El detall del qual estic més orgullós és com es llegeix el context del tenant. Les polítiques de RLS extreuen el tenant actual d'una variable de sessió a través de current_setting(..., true) — la forma amb dos arguments, que retorna NULL en lloc de llançar un error quan 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ó arriba d'alguna manera a la base de dades sense haver fixat app.tenant_id — un bug al middleware, un SET LOCAL oblidat, un job en segon pla que es va saltar la configuració de context — aquesta comparació s'avalua contra NULL, i tenant_id = NULL mai és cert per a cap fila. La consulta retorna zero files. No falla amb un error, i tampoc retorna accidentalment les dades de tots els tenants, que és el mode de fallada que obtindries amb un fallback ingenu de "si no hi ha tenant, mostra-ho tot". Tanca en fals per construcció, aplicat a nivell de base de dades sense importar per quin dels tres frontends en Next.js o per quina ruta de codi del backend hagi entrat la petició.

El servidor de teseŀles rep el mateix tractament des d'un altre angle. Martin serveix teseŀles vectorials MVT directament des de PostGIS, i les URL de MVT poden semblar recursos estàtics inofensius si no hi pares atenció — el tipus de cosa que és fàcil deixar accessible al món per accident. Vam posar Caddy al davant fent forward_auth, així que el mateix servidor de teseŀles no té cap accés públic sense autenticar.

4. El que està dissenyat però encara no construït

El model de dades ja té espai per a mòduls que encara no existeixen: cadastre (registre de la propietat), padró (el registre municipal de residents), activitats econòmiques (el registre d'activitats econòmiques locals) i 3D tiles. Cap està construït. Dissenyar l'esquema per a ells ara, en lloc de descobrir més endavant que la base no pot encaixar un mòdul de padró sense cirurgia, és la mateixa aposta que la multi-tenancy: pagar el preu de la generalitat aviat, mentre és barat, en les parts de les quals estàs raonablement segur que les necessitaràs.

Tancament

Nautaran no està publicat, no està provat a escala, i té exactament un tenant per ara. Però aquest tenant és un ajuntament real els 9.902 elements del qual portaven gairebé una dècada sense tocar-se, i tornar aquestes dades a un sistema que la gent fa servir de debò ja ha justificat la feina de la plataforma. Que la resta dels mòduls arribin a construir-se depèn de si més ajuntaments volen sumar-s'hi — però la base està construïda perquè puguin fer-ho, sense que haguem de reescriure res per permetre-ho.