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.
UrbanViable has a fundamentally small attack surface. There is no user authentication, no form submission, no database, and no server-side business logic. The scouting tool’s core interaction — weighting variables with sliders and filtering census sections — happens entirely in the browser. Slider values never leave the client. No user-generated input is sent to the server; the only requests the frontend makes are tile fetches and a single status JSON read. This eliminates the entire category of server-side injection vulnerabilities that are relevant to more complex applications.
CORS
When a /tiles/ reverse-proxy location is added to nginx/conf.d/default.conf, CORS headers should be set there to restrict tile consumption to the frontend’s own origin. In the current configuration TileServer GL is reachable on host port 8080 directly; adding a Nginx proxy layer is recommended before any production domain goes live. The CORS snippet for that location is:
location /tiles/ {
proxy_pass http://urbanviable-tiles:8080/data/;
# 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;
}
Allowed origins by environment
| Environment | Access-Control-Allow-Origin |
|---|
| Production | https://urbanviable.movilab.es |
| Local development (Vite dev server) | http://localhost:5173 |
Never use Access-Control-Allow-Origin: * in production. A wildcard origin allows any website on the internet to embed your tile source in a map and consume your server’s bandwidth without restriction. Set CORS_ORIGIN in .env to the exact domain of your frontend, and ensure the value is reflected in nginx/conf.d/default.conf.
HTTPS / TLS
| Aspect | Decision |
|---|
| Certificate provider | Let’s Encrypt (free, automatic renewal via Certbot) or Cloudflare origin certificate |
| SSL termination | Nginx (urbanviable-web container) |
| HTTP → HTTPS redirect | Automatic — the HTTP server block issues a 301 to the HTTPS equivalent URL |
| HSTS | Enabled — Strict-Transport-Security: max-age=31536000; includeSubDomains |
| TLS on TileServer GL | Not applicable — TileServer GL serves plain HTTP on port 8080; all public-facing TLS is handled by Nginx |
TLS certificates are bind-mounted into the Nginx container from ./certs/ as a read-only volume (/etc/letsencrypt:ro). The certs/ directory is excluded from Git via .gitignore — certificate files must never be committed to version control.
The following headers are applied to all responses from the urbanviable-web container:
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;
| Header | Value | Effect |
|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Browsers enforce HTTPS-only for one year; prevents SSL stripping and protocol downgrade attacks |
X-Content-Type-Options | nosniff | Prevents the browser from MIME-sniffing a response away from the declared Content-Type |
X-Frame-Options | DENY | Blocks the application from being embedded in an <iframe> on any other origin, preventing clickjacking |
X-XSS-Protection | 1; mode=block | Activates the legacy XSS filter in older browsers; modern browsers rely on CSP instead |
Content-Security-Policy
The CSP restricts what resources the browser is permitted to load, reducing the blast radius of any XSS vulnerability:
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https://*.basemaps.cartocdn.com https://*.openstreetmap.org;
connect-src 'self' https://tu-dominio.com;
font-src 'self' https://fonts.gstatic.com;
" always;
| Directive | Allowed sources | Reason |
|---|
default-src | 'self' | Baseline restriction: only same-origin resources unless overridden |
script-src | 'self' 'unsafe-inline' | Inline scripts are required by the Create React App bundle |
style-src | 'self' 'unsafe-inline' fonts.googleapis.com | Inline styles used by MapLibre GL; Google Fonts stylesheet |
img-src | 'self' data: *.basemaps.cartocdn.com *.openstreetmap.org | MapLibre GL loads basemap raster tiles from CartoDB (Dark Matter style); data: URIs used for sprites |
connect-src | 'self' https://tu-dominio.com | XHR/fetch is restricted to the same origin and the tile/status endpoints |
font-src | 'self' fonts.gstatic.com | Google Fonts font files |
The img-src directive must include CartoDB basemap tile domains (*.basemaps.cartocdn.com) because MapLibre GL JS requests raster tiles for the background map as <img>-equivalent network requests. If you switch basemap providers, update this directive accordingly.
Residual attack vectors
Despite its minimal server-side footprint, a few low-probability vectors exist:
| Vector | Risk level | Mitigation |
|---|
| Scraping or bulk-downloading tiles | Low–medium | Cache-Control reduces repeated server load; Cloudflare rate limiting can be added in front of Nginx if scraping becomes a concern |
| Direct access to TileServer GL | Low | TileServer GL binds to host port 8080. Restrict access at the firewall level (OCI Security List) to deny public ingress on port 8080, so external traffic can only reach tiles through Nginx on port 3002 |
Manipulation of last_update.json | Very low | The file is mounted into the Nginx container as a read-only bind mount (:ro); the container cannot write to it |
| URL parameter injection in tile requests | Very low | Nginx validates the /tiles/ path pattern before proxying; TileServer GL independently rejects any request that does not match a valid {z}/{x}/{y}.pbf pattern |
| Sensitive data exposure via tiles | None | Tiles contain only public, aggregated statistical data (INE census and income data); there is no PII, no user data, and no proprietary business data in the tile set |
ETL security
The ETL pipeline runs on a developer workstation, not on the production server. This architectural decision removes the most common server-side risks associated with data pipelines:
- No remote code execution surface on the server for ETL errors or dependency vulnerabilities
- No Python environment, pip packages, or ETL scripts exposed to the internet
- Data files travel to the server only via a direct
scp transfer from the developer’s machine
| ETL risk | Mitigation |
|---|
| Corrupted or malformed download from INE | Verify file size and structure before running process_data.py; review polygon count and null-value report before uploading |
Uploading an .mbtiles to the wrong server | Document and use a consistent SSH host alias; confirm the destination path before transfer |
| PII leaking through raw data files | INE data is statistically aggregated — no individual records, no names, no personal identifiers. raw/ is excluded from Git due to file size, not confidentiality |
GDPR / LOPDGDD applicability
UrbanViable does not collect, store, or process any personal data:
- No user accounts or login
- No analytics or tracking cookies
- No server-side logging of which zones a user views
- No transmission of slider weights to the server
- All tile data is derived from publicly available, aggregated statistical datasets (INE)
GDPR and LOPDGDD do not apply to the current MVP. If user accounts or analytics are added in the future, a full privacy impact assessment will be required.
Cache and tile security
# Tile responses are cached aggressively — tiles are static between ETL runs
add_header Cache-Control "public, max-age=86400"; # 24 hours
The 24-hour browser cache means tiles are not re-fetched on every page load, reducing server load and improving perceived performance. The trade-off is that after an ETL update, users may see stale tiles for up to 24 hours.
To force immediate cache invalidation after an ETL update, add a version query parameter to the tile URL before rebuilding the frontend:
// frontend/src/constants/variables.js
export const TILE_URL = `${import.meta.env.REACT_APP_TILE_URL}?v=2026`;
The ?v=2026 parameter is ignored by TileServer GL but causes the browser to treat the URL as a new, uncached resource. Increment the version number with each new annual ETL run.