| Versions | |
| CI | |
| Status | |
| Meta |
A lightweight, modular FastAPI application with Swagger UI, Docker support, Keycloak authentication, and Gunicorn + Uvicorn for production.
- Overview
- Compatibility
- Quick Start
- Configuration
- Environment variables reference
- Authentication
- Plugins
- Running with Docker
- Deployment Notes
- Deployment checklist (v1+)
- API Endpoints
- Project Structure
- Testing & Linting
- Releasing
- License
Giswater API exposes a clean HTTP surface to query and operate a Giswater database. It wraps existing GIS and network operations behind FastAPI endpoints, so desktop tools, mobile apps, and integrations can run the same workflows without directly touching the database.
How it works
- Each route validates input with Pydantic models, builds a standard payload, and calls Postgres procedures in the Giswater schema.
- Modules are toggled via environment variables, so you only expose the domains you need (Basic, OM, CRM, Routing, Water Balance).
- Optional Keycloak authentication protects routes while keeping Swagger/OpenAPI usable.
Why it is useful
- Centralizes complex GIS/business logic behind stable HTTP endpoints.
- Supports automated workflows (field operations, routing) without QGIS client coupling.
- Keeps API surface modular and controllable per deployment.
How to use it
- Base path:
${API_ROOT}/v1(default/giswater/v1; Swagger at${API_ROOT}/v1/docson each tenant host). Platform admin API:${API_ROOT}/admin(Swagger at${API_ROOT}/admin/docson the apex host only). OverrideAPI_ROOTto remount everything (e.g./gw-apifor legacy URLs). - Deploy behind nginx on the host: see
deploy/nginx.conf.example. The compose file binds the app to127.0.0.1:8000. - Tenants are resolved from the leftmost host label of
<tenant>.<BASE_DOMAIN>(e.g.acme.bgeo360.com). - Each tenant owns its own DB pool, Keycloak IDP, file/DB logger, and
API_*toggles viaconfig/tenants/<tenant>.env. - Use REST endpoints directly or via the autogenerated OpenAPI client.
This service is versioned alongside the Giswater database. Use matching ranges to avoid subtle schema/API mismatches.
giswater-api version |
Supported Giswater DB versions |
|---|---|
| 0.1 β 0.7 | 4.0 β 4.7 |
| 0.8 β 1.x | 4.8+ |
When GISWATER_DB_VERSION_CHECK=true, tenant readiness (GET ${API_ROOT}/v1/ready) returns 503 if {DB_SCHEMA}.sys_version does not report a giswater version β₯ GISWATER_DB_MIN_VERSION (default 4.8.0).
git clone https://github.com/Giswater/giswater-api.git
cd giswater-apicp .env.example .env
cp config/tenants/example.env config/tenants/test.envdocker compose up --build -dCheck service health:
curl http://127.0.0.1:8000/giswater/healthπ Tenant API docs (host-based): http://<tenant>.<BASE_DOMAIN>:8000/giswater/v1/docs (example: http://test.bgeo360.localhost:8000/giswater/v1/docs)
Tenant routes (including /giswater/v1/docs) require a tenant context. In browser, use host-based routing:
BASE_DOMAIN=bgeo360.localhostin.env- open
http://test.bgeo360.localhost:8000/giswater/v1/docs(for tenant fileconfig/tenants/test.env)
For API clients on localhost/IPs, set DEV_ALLOW_TENANT_HEADER=true and send X-Tenant-ID: <id>.
docker compose downpython3 -m venv venv
source venv/bin/activate # Windows PowerShell: .\venv\Scripts\activate
pip install -e ".[dev]"
uvicorn app.main:app --reloadAfter pip install -e ., the giswater-api console script reuses the same service layer as the HTTP API:
# List configured tenants
giswater-api admin tenants list
# Check tenant readiness
giswater-api tenant --tenant test --schema ws_40 ready
# Insert a hydrometer (example)
giswater-api tenant --tenant test --schema ws_40 --user postgres crm insert-hydrometer \
--code H1 --hydro-number HN1
# Database migrations for the API-owned gwapi schema
giswater-api db upgrade --all # or --tenant <id>
giswater-api db current --tenant test
giswater-api db historyUse --tenants-dir to override TENANTS_DIR when not running via the FastAPI lifespan.
The gwapi schema (basic-auth tables and audit logs) is managed by Alembic and, by default, migrated automatically on startup. See Database migrations.
Copy the env template and customize:
cp .env.example .env.env # global config only
config/tenants/<tenant_id>.env # one file per tenant (filename stem = tenant id = subdomain)
Template files named example.env, sample.env, and template.env are ignored by tenant discovery.
Tenant id rules: ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$. Reserved (rejected at load): www, api, admin, static, traefik, localhost.
Full reference (tables + explanations): docs/ENVIRONMENT_VARIABLES.md.
Templates: root .env.example, production deploy/.env.prod.example + deploy/main.env.example, per-tenant config/tenants/example.env.
Summary for quick orientation:
BASE_DOMAIN=bgeo360.com
TENANTS_DIR=config/tenants
API_ROOT=/giswater
LOG_DIR=logs
LOG_LEVEL=INFO
LOG_DB_ENABLED=true
LOG_DB_SAMPLE_RATE=1.0
LOG_HTTP_BODY_CAPTURE=true
LOG_DB_MAX_BODY_BYTES=2048
ADMIN_USER=admin
ADMIN_PASSWORD=<set me>
ADMIN_RELOAD_ENABLED=true
ADMIN_WRITE_ENABLED=true
DEV_ALLOW_TENANT_HEADER=false
PLATFORM_KEYCLOAK_ENABLED=false
PLATFORM_KEYCLOAK_URL=
PLATFORM_KEYCLOAK_REALM=
PLATFORM_KEYCLOAK_CLIENT_ID=
PLATFORM_KEYCLOAK_CLIENT_SECRET=
Gunicorn overrides and every variable name are documented in docs/ENVIRONMENT_VARIABLES.md and mirrored in .env.example.
Per tenant (config/tenants/<id>.env) β API_* toggles, DB pool, optional Keycloak. See config/tenants/example.env and docs/ENVIRONMENT_VARIABLES.md.
Each tenant has its own pool. Total connections to your Postgres β N_tenants Γ DB_POOL_MAX_SIZE (defaults to 10). Plan accordingly.
GET /admin/tenantsβ list tenants (secrets redacted)GET /admin/tenants/{id}β read onePOST /admin/tenantsβ create + write.envatomicallyPUT /admin/tenants/{id}β full replace; secrets unset = keep existingDELETE /admin/tenants/{id}β drains pool, archives.envto_archive/POST /admin/tenants/{id}/reloadβ re-read one.envfrom diskPOST /admin/tenants/reloadβ rescanTENANTS_DIR
All /admin/* endpoints support either auth path: HTTP Basic with ADMIN_USER/ADMIN_PASSWORD, or a Bearer JWT from the platform Keycloak realm with role platform-admin.
Mutations gated by ADMIN_WRITE_ENABLED=true; full-dir reload also gated by ADMIN_RELOAD_ENABLED=true.
Tenant API auth is controlled per tenant by AUTH_MODE:
| Mode | Behavior |
|---|---|
none (default) |
Anonymous access β same as 1.3.x with KEYCLOAK_ENABLED=false |
keycloak |
Bearer JWT from the tenant Keycloak realm |
basic |
HTTP Basic (Authorization: Basic β¦) validated against gwapi.users in the tenant DB |
Migration from 1.3.x: no config change required. KEYCLOAK_ENABLED=true β keycloak, false β none. Set AUTH_MODE explicitly when convenient; KEYCLOAK_ENABLED is deprecated (removed in 2.0.0).
Basic auth: users live in the tenant Postgres (gwapi schema). Each user has a db_role that must exist as a PostgreSQL role (SET ROLE). Manage users via admin API: ${API_ROOT}/admin/tenants/{id}/users (requires tenant AUTH_MODE=basic).
Optional bootstrap: AUTH_BASIC_BOOTSTRAP_USER / AUTH_BASIC_BOOTSTRAP_PASSWORD create the first user when the table is empty.
Role checks: inject Depends(require_role("role_om", "role_master")) on routes; roles come from Keycloak JWT claims or gwapi.user_roles in basic mode.
Keycloak integration details (unchanged for AUTH_MODE=keycloak):
- Endpoints require a valid
Authorization: Bearer <token>issued by the tenant's realm. - Tokens are decoded against the tenant idp's RS256 public key (no realm round-trip per request).
When disabled (AUTH_MODE=none), endpoints accept anonymous requests for that tenant.
/admin/* uses a separate platform Keycloak realm (PLATFORM_KEYCLOAK_*) plus HTTP Basic, with platform-admin as the required role.
Swagger's built-in OAuth2 helper is no longer wired in β it can only point at one realm at a time, which is incompatible with multi-tenant. Clients send
Authorization: Bearer ...directly.
Custom plugins can extend the API. Place plugin folders in plugins/:
plugins/
βββ my-plugin/
βββ ...
Plugins are auto-loaded at startup on the tenant API surface (${API_ROOT}/v1, default /giswater/v1; same feature toggles and OpenAPI as core routers). See example plugin.
Use Compose as the default runtime:
docker compose up --build -d
docker compose ps
docker compose logs -f appDev hot reload:
cp docker-compose.override.yml.example docker-compose.override.yml
docker compose up --buildThe override enables:
- bind mount
.:/app uvicorn --reloadDEV_ALLOW_TENANT_HEADER=true
On a Debian/Ubuntu server with Docker:
curl -fsSL https://raw.githubusercontent.com/Giswater/giswater-api/main/deploy/install.sh | sudo bashCreates /opt/giswater-api with production Compose, .env (SINGLE_TENANT_ID=main), and config/tenants/main.env. See deploy/README.md.
Pin deploy templates: GISWATER_API_REF=v1.3.2 curl -fsSL ... | sudo bash (Docker image tag is prompted separately).
- Keep
proxy_set_header Host $hostat the reverse proxy (deploy/nginx.conf.example) because tenant resolution depends onHost. - Apex host (
BASE_DOMAIN) serves only${API_ROOT}/admin/*; tenant hosts (<tenant>.<BASE_DOMAIN>) serve${API_ROOT}/v1/*. - Base compose binds to
127.0.0.1:8000; expose publicly through your proxy/TLS layer. - Production images start Gunicorn +
uvicorn.workers.UvicornWorker(seegunicorn.conf.py,Dockerfile). Override worker count withWEB_CONCURRENCYif needed. - Tune container/Kubernetes probes: allow enough
start_periodfor Postgres pool init;${API_ROOT}/healthis fast; tenant${API_ROOT}/v1/readyvalidates DB connectivity (and optional DB version check).
| Variable | Default | Meaning |
|---|---|---|
LOG_HTTP_BODY_CAPTURE |
true |
When true, request/response payload text is logged for failed requests (4xx/5xx) with redaction + truncation; binary/multipart payloads are skipped. |
LOG_DB_MAX_BODY_BYTES |
2048 |
Max bytes per stored body (0 uses the same safe internal cap). |
LOG_DB_ENABLED |
true |
Sample API rows into the tenant log table. |
LOG_DB_SAMPLE_RATE |
1.0 |
Fraction of tenant requests logged to DB (1.0 = all; typical for QGIS plugin workloads). |
Detailed reference: docs/ENVIRONMENT_VARIABLES.md. Production installer: deploy/install.sh. Copy-paste templates: .env.example, deploy/.env.prod.example. Operator checklist: docs/DEPLOYMENT_CHECKLIST.md. Per-tenant template: config/tenants/example.env.
All surfaces live under ${API_ROOT} (default /giswater; override via the API_ROOT env var).
- Tenant surface prefix:
${API_ROOT}/v1(default/giswater/v1) - Admin surface prefix:
${API_ROOT}/admin(default/giswater/admin, apex host only) - Health checks:
${API_ROOT}/health(global, tenant-independent)${API_ROOT}/v1/health(tenant scope)${API_ROOT}/admin/health(admin scope)
- OpenAPI docs:
- Tenant:
${API_ROOT}/v1/docson tenant host - Admin:
${API_ROOT}/admin/docson apex host
- Tenant:
- Module routers are loaded from:
basicom(profile,flow,mincut,waterbalance,mapzones)routingcrmepa(dscenario)system(ready, schema validation, tenant-scoped logs)
Use OpenAPI as source of truth for the full endpoint list in your running environment.
giswater-api/
βββ app/
β βββ main.py # FastAPI app entry point (lifespan, sub-app mounts, middleware)
β βββ api/ # HTTP layer
β β βββ deps.py # Shared FastAPI dependencies (common_parameters, get_schema, require_feature)
β β βββ exception_handlers.py # Service error β HTTP response mapping
β β βββ v1/ # Versioned tenant API
β β β βββ router.py # Router wiring + per-tenant OpenAPI filter
β β β βββ endpoints/ # Thin handlers; delegate to services/
β β βββ admin/ # Platform-admin API (tenants.py, users.py, router.py)
β β
β βββ services/ # HTTP-agnostic business logic (API + CLI)
β βββ cli/ # Click CLI (`giswater-api` entry point)
β β
β βββ core/ # Dependency-free leaf: config.py, constants.py, exceptions.py
β βββ auth/ # session.py, keycloak.py, users.py, schemas.py, constants.py
β βββ db/ # manager.py, context.py, execution.py, version.py, log_store.py, schema.py, partitions.py, migrate.py
β βββ tenancy/ # registry.py, state.py, host_middleware.py
β βββ middleware/ # request_logging.py
β βββ schemas/ # Pydantic request/response models (basic/, crm/, om/, routing/, epa/, admin.py, common.py)
β βββ utils/ # body.py, version.py, rate_limit.py, plugins.py, log_setup.py, routing.py
β βββ static/ # Static files (favicon, logs UI, etc.)
β
βββ alembic/ # gwapi schema migrations (env.py + versions/)
βββ alembic.ini # Alembic config (dev CLI; runtime config is programmatic)
βββ config/
β βββ tenants/ # Per-tenant .env files (e.g. test.env, acme.env)
βββ deploy/
β βββ nginx.conf.example
βββ plugins/ # Plugin directory (see plugins/readme.md)
βββ tests/ # Tests
βββ .github/workflows/ # CI/CD (ruff, pytest)
βββ Dockerfile # Docker build config
βββ docker-compose.yml
βββ docker-compose.override.yml.example
βββ gunicorn.conf.py # Gunicorn + Uvicorn worker defaults for production images
βββ pyproject.toml # Project metadata, dependencies, and tooling config
βββ docs/
β βββ ARCHITECTURE.md # Package map, dependency rules, code locations
β βββ VERSIONING.md # API + per-tenant DB versioning policy
β βββ DATABASE_MIGRATIONS.md # Alembic gwapi schema migrations + upgrade paths
β βββ DEPLOYMENT_CHECKLIST.md
β βββ ENVIRONMENT_VARIABLES.md # Human-readable env reference (tables + descriptions)
βββ scripts/
β βββ release.sh
β βββ release.ps1
β βββ smoke_test.sh
βββ README.md
pytestruff check .CI/CD runs both on push via GitHub Actions (.github/workflows/).
- Update
CHANGELOG.mdunder[Unreleased]and move the block to a dated## [X.Y.Z]section; commit. - Follow docs/DEPLOYMENT_CHECKLIST.md for production cutovers.
- Run the release script:
# Bash
./scripts/release.sh 1.0.0
# PowerShell
.\scripts\release.ps1 1.0.0The script will:
- Abort if there are uncommitted changes
- Update the version in
pyproject.toml - Commit, tag
vX.Y.Z, create branchrelease/X.Y - Push everything to origin
This project is free software, licensed under the GNU General Public License (GPL) version 3 or later. Refer to the LICENSE file for details.