Building GISOwl on Go, PostgreSQL, and React: Architecture Decisions

March 2026

Most web-based mapping platforms are built on Python with GeoDjango, or Node.js with Mapbox GL wrappers. GISOwl isn't. Our backend is Go with the Echo framework, our database is PostgreSQL with PostGIS and pgvector extensions, and our frontend is React. Here's why we made these choices and what we learned.

Why Go

Geospatial backends are deceptively compute-heavy. Every map view triggers tile rendering, permission checks, and layer composition. Every spatial query hits geometric intersections across millions of features. Go's concurrency model — goroutines and channels — handles this naturally.

Our API response times sit at p50 under 15ms and p99 under 80ms for authenticated endpoints. We didn't have to think about async frameworks or event loops; Go's scheduler handles the concurrency we need out of the box.

The other advantage is deployment simplicity. A Go binary is a single static file. We deploy on Dokku, and our entire backend is one container with no runtime dependencies beyond PostgreSQL.

Why PostgreSQL (with PostGIS and pgvector)

Every mapping platform eventually needs three things beyond basic CRUD: spatial queries, full-text search, and vector similarity for AI features. PostgreSQL handles all three natively with extensions.

PostGIS is the backbone of GISOwl — it handles geometry storage, spatial indexing, intersection queries, buffer analysis, coordinate transformations, and raster operations. Features like "find all parcels within 500m of this river" or "calculate the area of these overlapping polygons" run as standard SQL.

pgvector powers our Owl AI features — we store embeddings for map descriptions, layer metadata, and user queries, enabling semantic search ("find maps similar to this environmental survey") without a separate vector database.

We use sqlx for database access with a codegen pattern: standard CRUD operations are generated from schema definitions, while business logic lives in custom controllers. Table names follow an hh_ prefix convention for namespacing.

Why React (Not a SPA Framework)

Our frontend is React with server-side rendered marketing pages served from the Go backend. The mapping application itself is a SPA at app.gisowl.com, but marketing pages are pre-rendered HTML templates served directly by Echo.

This split — SSR for marketing, SPA for app — gives us the best of both worlds: crawlable marketing pages for SEO and a fast, interactive mapping experience for logged-in users. The map editor uses deck.gl and Mapbox GL JS under the hood.

Tile Serving and Layer Rendering

One of the most critical architectural decisions was how to serve map tiles. We generate vector tiles on the fly from PostGIS using ST_AsMVT, cached at the edge. This means uploaded data is immediately visible on the map without a pre-processing pipeline.

For raster imagery, we use a tile server that reads Cloud Optimized GeoTIFFs (COGs) directly from object storage, serving only the pixels needed for the current viewport and zoom level.

What We'd Do Differently

If starting over, we'd invest in OpenAPI spec generation from day one. Our API grew organically and the documentation lagged behind. We're now retroactively building a Swagger spec, but it would have been cheaper to generate it from annotated handlers from the start.

We'd also set up robots.txt and sitemap.xml as static files served outside the Echo router from day one. Routing these through Echo's catch-all handler caused a subtle bug where 404s returned JSON instead of HTML — something that went unnoticed for months.

signature here. ...