All posts

Beyond Software: Building nautaran, a GIS Platform for Municipal Infrastructure

Published Jul 17, 20265 min read
  • Product
  • Engineering
  • GIS

Key takeaways

  • Nautaran is Maladeta's first B2G product: a multi-tenant GIS platform for town councils to manage water, sewage, stormwater and lighting networks.
  • The platform is built multi-tenant from day one — nothing hardcodes the first tenant, Ajuntament de Naut Aran — with optional modules enabled per tenant.
  • Tenant isolation is enforced with PostgreSQL row-level security using a fail-closed current_setting(..., true) pattern, not just an ORM-level scope.

Every product Maladeta Studio has shipped so far has had one thing in common: an end user who chose to be there. Lueira's guests book a ski lesson because they want to ski. MeteoTrail's hikers check a forecast because they're planning a route. Even the marketplace platform, in whatever shape it takes for a given client, exists because someone wanted to buy or sell something. Nautaran breaks that pattern. Nobody signed up for it — they inherited a filing cabinet.

That's not a metaphor. Between 2017 and 2018, someone surveyed the water, sewage, stormwater and street lighting networks of a municipality in Val d'Aran: 9,902 geographic features, walked and mapped by hand. Then it sat. No maintenance, no updates, slowly drifting away from what's actually buried in the ground. That's the starting point for nautaran (internal codename: Urban), a new B2G product line for the studio — software built for town councils (ajuntaments), not their residents.

It's a different kind of project than anything we've built before, and I want to write about the decisions that made it feel like a real product line instead of a one-off GIS job for a client.

1. From ski schools to town councils: a new kind of customer

The studio's civic-tech instincts aren't new — the NaturPark work and the parking and waste posts from a while back already pointed this way. Nautaran is the first time we've turned that instinct into a formal, sellable product instead of a one-off project.

The customer is an ajuntament, and the "user" inside it splits into at least two people: an office worker who needs to know where a valve is before signing off on a dig permit, and a brigada — the maintenance crew — who needs the same information standing in a trench with wet gloves and no signal. Designing for both from day one shaped almost every technical decision that followed, starting with the fact that this couldn't be a single application.

2. A base platform plus modules, not features hardcoded to one tenant

Naut Aran is our first tenant, but nothing in the codebase assumes it's the only one. That's a lesson we already learned the hard way with Lueira's own multi-tenancy work — retrofitting isolation onto something written tenant-by-tenant hurts more than doing it properly up front. So nautaran is multi-tenant from the first migration: auth, role-based access control, audit logging, file storage, a layer/type/element data model, map tile serving, a tenant backoffice and an internal Maladeta console all ship as one base platform, sold as a unit to every ajuntament that comes on board.

On top of that base sit modules, enabled per tenant. Two exist today. serveis_publics is the "visor" — a combined map and table view for browsing the water, sewage, stormwater and lighting network inventory. app_camp is an offline-capable Progressive Web App built for the brigada: it has to work when the crew is standing next to a manhole cover with no connectivity, so the data model and sync logic were designed around eventual consistency from the start.

A module can be switched off for a tenant without touching its data. Disabling app_camp for a council that isn't ready for it just makes its routes, tiles and navigation entries stop resolving — nothing gets deleted. It's the same toggleable-module idea from NaturPark, applied to something with real operational stakes: a council can pilot a module and turn it back off if it doesn't fit, without ever risking survey data.

3. Why row-level security instead of just an ORM scope

Lueira's multi-tenant isolation relies on an ORM-level default scope with a database constraint as a backstop — reasonable for a platform where the worst-case leak is someone seeing another school's ski lesson bookings. Nautaran's worst case is a lot worse: a leaked water or sewage network map for the wrong municipality is genuinely sensitive infrastructure data, not an embarrassment. That difference in stakes is why I leaned on PostgreSQL's native row-level security as the primary enforcement mechanism rather than a second line of defense.

The detail I'm proudest of is how the tenant context is read. RLS policies pull the current tenant from a session variable through current_setting(..., true) — the two-argument form, which returns NULL instead of raising when the variable isn't set:

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
    );

If a request somehow reaches the database without setting app.tenant_id — a bug in middleware, a forgotten SET LOCAL, a background job that skipped context setup — that comparison evaluates against NULL, and tenant_id = NULL is never true for any row. The query returns zero rows. It doesn't error out, and it doesn't accidentally return every tenant's data either, which is the failure mode you'd get with a naive "if no tenant, show everything" fallback. Fail-closed by construction, enforced at the database level regardless of which of the three Next.js frontends or which backend code path the request came through.

The tile server gets the same treatment from a different angle. Martin serves MVT vector tiles straight from PostGIS, and MVT URLs can look like harmless static assets if you're not paying attention — the kind of thing that's easy to leave world-readable by accident. We put Caddy in front of it doing forward_auth, so the tile server itself has no public unauthenticated access at all.

4. What's designed but not yet built

The data model already has room for modules that don't exist yet: cadastre (land registry), padró (the municipal resident registry), activitats_economiques (the local business registry) and 3D tiles. None are built. Designing the schema for them now, rather than discovering later the base can't fit a padró module without surgery, is the same bet as multi-tenancy: pay for generality early, while it's cheap, on the parts you're fairly confident you'll need.

Closing

Nautaran is unreleased, unproven at scale, and has exactly one tenant so far. But that tenant is a real council whose 9,902 features had been sitting untouched for the better part of a decade, and getting that data back into a system people actually use has already justified the platform work. Whether the rest of the modules ever get built depends on whether more ajuntaments want in — but the base is built so that they can, without us rewriting anything to let them.