Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/danizd/geoviable/llms.txt

Use this file to discover all available pages before exploring further.

GeoViable is deployed as a three-container Docker Compose application running on an Oracle Cloud Always Free ARM instance. All external traffic passes through Cloudflare before reaching the host, where Nginx terminates SSL and routes requests — API calls to the FastAPI container, everything else to the pre-compiled React static files. The PostGIS database sits entirely within the internal Docker network and is never exposed to the internet. A cron job running inside the API container handles automated monthly layer updates without any external scheduler.

High-Level Architecture

Internet → Cloudflare (DNS + proxy) → Oracle Cloud VM :443
  → Nginx (SSL termination + reverse proxy)
    → /api/*  → geoviable-api:8000   (FastAPI + WeasyPrint + cron)
    → /*      → React static files   (/usr/share/nginx/html)

FastAPI ↔ geoviable-db:5432 (PostgreSQL 15 + PostGIS 3.4)
Cron Job → update_layers.py → monthly automated layer refresh

Docker Containers

All three containers are defined in docker-compose.yml and communicate exclusively over the internal geoviable-net bridge network.
ContainerBase imageInternal portPurpose
geoviable-dbpostgis/postgis:15-3.45432PostgreSQL 15 + PostGIS 3.4 spatial database
geoviable-apipython:3.11-slim (custom build)8000FastAPI application, WeasyPrint PDF renderer, cron daemon
geoviable-webpublic.ecr.aws/docker/library/nginx:1.25-alpine80Reverse proxy + React static file serving

Docker network

All containers are attached to a single bridge network named geoviable-net:
networks:
  geoviable-net:
    driver: bridge
Containers resolve each other by service name (e.g. the FastAPI app connects to geoviable-db:5432). Only geoviable-web exposes ports to the host — the database and API containers are not reachable from outside the Docker network in the default configuration.

Persistent volumes

VolumeMount pathPurpose
pgdata (named)/var/lib/postgresql/dataPersistent PostgreSQL data — survives container restarts
./frontend/build (bind)/usr/share/nginx/htmlPre-compiled React static assets served by Nginx
./certs (bind)/etc/letsencryptLet’s Encrypt SSL certificates

Full Analysis Flow

The following sequence describes every step from the moment a user submits a polygon to the moment a PDF lands in their browser.
  1. User draws or uploads a polygon in the React + Leaflet interface. GeoJSON files and Shapefiles are parsed entirely client-side — no file is sent to the server at this stage.
  2. Frontend validates the geometry: exactly 1 polygon, area < 100 km², vertex count < 10,000. Invalid geometries are rejected before any network request is made.
  3. POST /api/v1/analyze — the frontend sends the validated polygon as a GeoJSON Feature in EPSG:4326 (WGS84 longitude/latitude).
  4. FastAPI reprojects to EPSG:25830 using PostGIS ST_Transform. All stored environmental data is in ETRS89 / UTM zone 30N (EPSG:25830), so reprojection must happen before any spatial comparison.
  5. PostGIS runs ST_Intersects + ST_Area against all seven environmental layers simultaneously. For each layer that intersects, the query also computes the intersection geometry, the area of that intersection in m², and the percentage of the parcel covered.
  6. Results returned as JSON — the API response includes a per-layer breakdown of intersecting features, areas, and overlap percentages. The frontend overlays these geometries on the map using coloured polygons.
  7. User clicks “Generate report” — the frontend calls POST /api/v1/report/generate, sending the same GeoJSON polygon plus the project name and metadata.
  8. Backend generates the PDF:
    • Re-runs the spatial analysis (for data consistency and PDF autonomy).
    • Generates a static basemap image using contextily (tile download), matplotlib (rendering), and geopandas (vector overlay).
    • Renders the full report HTML from a Jinja2 template, embedding the basemap, all layer results, the risk score, and project metadata.
    • Converts the HTML to PDF using WeasyPrint.
  9. PDF returned as application/pdf — the browser triggers an automatic file download.

PostGIS Query Pattern

The spatial query follows the same pattern for each of the seven layers. The example below shows the Red Natura 2000 query as it appears in the system specifications:
-- Example: cross-reference with the Red Natura 2000 layer
SELECT
    rn.nombre,
    rn.tipo,                              -- 'ZEPA' or 'LIC/ZEC'
    rn.codigo,
    ST_Area(ST_Intersection(rn.geom, ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:user_geojson), 4326), 25830))) AS area_interseccion_m2,
    ST_Area(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:user_geojson), 4326), 25830)) AS area_parcela_m2,
    ROUND(
        100.0 * ST_Area(ST_Intersection(rn.geom, ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:user_geojson), 4326), 25830)))
        / ST_Area(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:user_geojson), 4326), 25830)),
        2
    ) AS porcentaje_solape
FROM red_natura_2000 rn
WHERE ST_Intersects(
    rn.geom,
    ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON(:user_geojson), 4326), 25830)
);
The key functions in play:
  • ST_GeomFromGeoJSON — parses the raw GeoJSON string into a PostGIS geometry.
  • ST_SetSRID(..., 4326) — tags the geometry with its source CRS (WGS84).
  • ST_Transform(..., 25830) — reprojects to ETRS89 / UTM zone 30N for metric area calculations.
  • ST_Intersects — boolean spatial predicate; leverages the GIST index for performance.
  • ST_Intersection — computes the actual overlapping geometry for area measurement.
  • ST_Area — returns the area in square metres (since CRS is metric).

Coordinate Reference Systems

GeoViable uses three different CRS at different stages of the pipeline:
StageCRSEPSGReason
User input (browser)WGS84 geographicEPSG:4326Standard for GeoJSON and web maps
Stored layer dataETRS89 / UTM zone 30NEPSG:25830Spanish national standard; metric units for area calculation
Map tile displayWeb MercatorEPSG:3857Required by OpenStreetMap / Leaflet tile providers
All reprojection between 4326 and 25830 is handled server-side by PostGIS ST_Transform at query time, so the client never needs to perform coordinate transformations.

Monthly Automated Data Update

The geoviable-api container runs a cron daemon alongside the Uvicorn server (configured via backend/scripts/crontab and launched by backend/scripts/entrypoint.sh).
AspectDetail
Scriptbackend/scripts/update_layers.py
ScheduleDay 1 of every month at 03:00 UTC
Execution contextInside the geoviable-api container
Download methodrequests + BeautifulSoup to locate download links on MITECO/CNIG pages; Playwright headless fallback for JavaScript-rendered pages
The update flow for each layer:
  1. Download the Shapefile/ZIP from the official MITECO or CNIG source URL.
  2. Decompress and read with GeoPandas.
  3. Reproject to EPSG:25830 if the source CRS differs.
  4. Filter records to the Galicia bounding box.
  5. TRUNCATE + INSERT inside a single SQL transaction — if anything fails, ROLLBACK preserves the previous data intact.
  6. Run VACUUM ANALYZE on the updated table.
  7. Record the result (success or failure, timestamp, row count) in the layer_update_log table.
# Pseudocode for the transactional update strategy
with db.begin():
    db.execute(f"TRUNCATE TABLE {table_name}")
    gdf.to_postgis(table_name, db, if_exists="append", index=False)
# ROLLBACK happens automatically if any exception is raised
If a layer download fails — for example, because an upstream MITECO URL has changed — the cron job logs the error to layer_update_log and leaves the existing data untouched. The system continues serving analyses with the previous dataset until the next successful update.
You can also trigger a manual update at any time:
docker compose exec geoviable-api python -m scripts.update_layers

Dual-Endpoint Design

GeoViable intentionally uses two separate endpoints in the standard user workflow (see ADR-001 in the source specifications):
  • POST /api/v1/analyze — runs the spatial analysis and returns JSON for map overlay. Used for interactive preview.
  • POST /api/v1/report/generate — runs the full analysis again internally, generates the static map, and returns a PDF binary. The re-execution guarantees that the PDF is always self-consistent and does not depend on a prior /analyze call.
This design means the PDF report can be generated independently (e.g. via API automation) without a preceding map interaction.

Where to go next

Docker Setup

Production deployment on Oracle Cloud, SSL configuration with Let’s Encrypt, and Nginx Proxy Manager setup.

Updating Layers

Manual and automated environmental data loading, the TRUNCATE+INSERT strategy, and troubleshooting failed updates.

Build docs developers (and LLMs) love