Skip to main content

Documentation Index

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

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

Nginx (urbanviable-web) is the public entry point for UrbanViable. The current nginx/conf.d/default.conf serves three concerns from a single HTTP virtual host: the compiled React SPA, a static JSON data-freshness endpoint mapped from last_update.json, and direct static file serving of tile data. TileServer GL handles its own port (8080) separately on the same host.

nginx/conf.d/default.conf

server {
    listen 80;
    server_name _;

    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }

    location /api/status {
        alias /data/last_update.json;
        add_header Content-Type application/json;
        add_header Cache-Control "public, max-age=3600";
    }

    location /data/ {
        alias /data/;
        add_header Content-Type application/json;
        add_header Cache-Control "public, max-age=3600";
    }
}

Location blocks explained

location / — React SPA fallback

location / {
    root /usr/share/nginx/html;
    try_files $uri $uri/ /index.html;
}
Serves static files from the compiled React build (frontend/build/ on the host, mounted read-only into /usr/share/nginx/html). The try_files directive implements the SPA routing pattern: Nginx first tries to serve the exact path as a file, then as a directory, and finally falls back to /index.html. This ensures that client-side routes such as /scouting or /how-to-use are handled by React Router rather than returning a 404.

location /api/status — static data freshness endpoint

location /api/status {
    alias /data/last_update.json;
    add_header Content-Type application/json;
    add_header Cache-Control "public, max-age=3600";
}
Serves the last_update.json file generated by the ETL pipeline. The frontend’s DataStatus component polls this endpoint to display when the underlying census data was last refreshed. The file is available inside the container at /data/last_update.json via the ./tiles_data:/data:ro bind mount. Responses are cached for 1 hour (max-age=3600).

location /data/ — static tile data files

location /data/ {
    alias /data/;
    add_header Content-Type application/json;
    add_header Cache-Control "public, max-age=3600";
}
Serves files directly from the /data/ directory inside the container, which is the same ./tiles_data bind mount. This location exposes JSON metadata files and other static data assets that the frontend or developer tooling may request.

SSL configuration

The current nginx/conf.d/default.conf listens on HTTP port 80. The ./certs directory is bind-mounted into the container at /etc/letsencrypt:ro in anticipation of TLS termination. To enable HTTPS, replace the single server block with a redirect server and a TLS server:
server {
    listen 80;
    server_name tu-dominio.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name tu-dominio.com;

    ssl_certificate     /etc/letsencrypt/live/tu-dominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tu-dominio.com/privkey.pem;

    # ... location blocks
}
Certificates can be obtained via Certbot (recommended for bare OCI instances) or delegated to Cloudflare if the domain is proxied through Cloudflare’s network, in which case Nginx uses the Cloudflare origin certificate. After adding TLS, enable HSTS to prevent protocol-downgrade attacks:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

Adding a /tiles/ reverse proxy

The Vite dev server (vite.config.js) already proxies /tiles to http://127.0.0.1:8080/data for local development. To mirror this in the production Nginx config, add a proxy location to nginx/conf.d/default.conf:
location /tiles/ {
    proxy_pass http://urbanviable-tiles:8080/data/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # CORS — only the frontend's own domain may consume tiles
    add_header 'Access-Control-Allow-Origin' 'https://tu-dominio.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

    # Aggressive cache — tiles are static between ETL runs
    add_header Cache-Control "public, max-age=86400";
}
The /tiles/ location strips the /tiles/ prefix when proxying. The URL pattern the frontend requests is:
GET /tiles/galicia-scouting/{z}/{x}/{y}.pbf
which Nginx maps internally to:
http://urbanviable-tiles:8080/data/galicia-scouting/{z}/{x}/{y}.pbf
The trailing slash on both location /tiles/ and proxy_pass http://urbanviable-tiles:8080/data/ is required for this prefix substitution to work correctly. Removing either slash will break all tile requests.
After adding this location, restart the Nginx container:
docker compose restart urbanviable-web

HTTP security headers

The following headers should be applied in the main server block so they are present on every response, regardless of location:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
HeaderValuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS for one year; prevents protocol downgrade
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing attacks
X-Frame-OptionsDENYBlocks the site from being embedded in an iframe
X-XSS-Protection1; mode=blockLegacy XSS filter for older browsers

Cache invalidation after ETL updates

The /data/ and /api/status locations cache responses for 1 hour. After an ETL update, users will automatically receive fresh data within that window. To force an immediate client-side cache bust for tile URLs, update the REACT_APP_TILE_URL variable in .env with a version query parameter before rebuilding the frontend:
# .env
REACT_APP_TILE_URL=https://tu-dominio.com/tiles?v=2026

# Rebuild and redeploy the frontend
cd frontend && npm run build && cd ..
docker compose restart urbanviable-web
The ?v=2026 parameter is ignored by TileServer GL but is treated as a distinct URL by the browser cache, so all clients will fetch fresh tiles on the next page load. Increment the version number with each new annual ETL run.

Build docs developers (and LLMs) love