Skip to content

Architecture

Components

flowchart LR
    B[Browser / API client] --> RP[External reverse proxy]
    RP --> P[qgis-publish-service]
    P --> UI[Static publish UI]
    P --> C[(Central PostgreSQL)]
    P --> FS[Project and dataset files]
    P --> Q[QGIS Server CGI/HTTP]
    P --> T[qcarta-tiles]
    P --> G[GeoVault]
    P --> O[OSRM]
    P --> GDAL[GDAL/OGR and PyQGIS processes]
    T --> Q
    T --> TC[Disk tile/metadata cache]
    B --> CDN[Public UI CDNs and epsg.io]

The main process uses Go's net/http ServeMux. It serves the UI, API routes, shared UI assets imported from ../shared, and reverse proxies. There is no separate application worker process or message broker.

Request paths

  • Management routes use BASE_PATH and service-level qgis_publish ACLs when Central auth is configured.
  • Project read/write routes additionally evaluate qgis_project resources.
  • /qgis/{projectId} validates the project ID, resolves the server-side project path, validates filter parameters, checks project view, and either executes QGIS_MAPSERV_BIN or forwards to QGIS_MAPSERV_URL.
  • /cache/* strips the publish cache prefix and proxies to qcarta.
  • /routing/* strips the routing prefix and proxies to OSRM.
  • GeoVault browse/quick-import routes allow only specific path shapes and forward the Central session context.
sequenceDiagram
    participant Client
    participant Publish as qgis-publish-service
    participant Auth as Central DB
    participant QGIS as QGIS Server
    Client->>Publish: GET /qgis/{project}?SERVICE=WMS...
    Publish->>Auth: session and project ACL lookup
    Auth-->>Publish: user/groups/permission
    Publish->>Publish: resolve manifest and validate filters
    Publish->>QGIS: WMS with server-controlled MAP
    QGIS-->>Publish: map response
    Publish-->>Client: filtered upstream response

Publishing pipeline

Uploads are capped at 512 MiB by default. A temporary file is created, then:

  1. .qgs is moved into a generated project directory.
  2. .qgz or ZIP content is extracted and flattened where applicable.
  3. .qgis-publish.json records project paths, ownership, render mode, forms, relations, layer configuration, and runtime dataset references.
  4. Forms and layer/legend configuration are extracted from QGIS XML.
  5. A Central qgis_project resource and default grants are created.
  6. qcarta metadata prewarm starts asynchronously.

Updates snapshot the complete current project directory before replacement. Snapshots are filesystem copies, not Git revisions or database records.

Operational import

The legacy GeoPackage flow is synchronous in the HTTP request:

  1. Save the upload to an OS temporary file.
  2. Create a dedicated PostgreSQL database and enable PostGIS.
  3. Create/reuse a runtime role and grant database/schema/table/sequence privileges.
  4. Write a libpq service entry.
  5. Inspect/import every layer with ogrinfo/ogr2ogr.
  6. Record dataset, table, and relation registry rows.
  7. Archive the GeoPackage and optionally generate/install a QGIS project.

Failures mark the registry row failed, but source does not implement full rollback of a created database, role, service entry, or partially imported data.

Jobs and concurrency

  • Main-service qcarta metadata prewarm uses an untracked goroutine after publish/update. It is not durable and is lost on process exit.
  • qcarta seed jobs run in goroutines and are stored only in an in-memory map. They are capped at 300,000 tiles, processed serially per job, have no persistence/retention cleanup, and disappear on restart.
  • qcarta limits concurrent upstream QGIS requests with a six-slot in-process semaphore.
  • Metatile generation uses singleflight so matching concurrent requests share one upstream render.
  • No cron scheduler, external queue, distributed lock, or multi-instance job coordination is present.

Caching

qcarta hashes normalized WMS dimensions into project-scoped disk paths. It caches PNG/JPEG/XML responses, canonical tile slices, and WMS GetCapabilities/GetProjectSettings metadata. The default TTL is seven days for normal response cache reads. Metadata cache reads do not enforce that TTL in the current source.

Cache metrics (hits, misses, bypass) are process-local counters exposed as JSON. Purge is disk deletion protected by a shared bearer token. Browser responses receive X-Cache; tile responses also include a SHA-256 identity header.

Storage

  • Projects: PROJECTS_ROOT/<project-id>/, including QGIS files, data, reports.json, and .qgis-publish.json.
  • Versions: GEOVAULT_PROJECT_VERSIONS_ROOT/<project-id>/vN/ plus .version-meta.json.
  • Runtime datasets: RUNTIME_DATASETS_ROOT/<dataset-uuid>/.
  • Ingest temporary data: INGEST_TEMP_ROOT/<session-uuid>/.
  • Browser thumbnails: BROWSER_MAP_THUMBNAILS_DIR or the UI tree.
  • PostgreSQL service credentials: PG_SERVICE_FILE.
  • qcarta cache: QCARTA_CACHE_DIR.

These stores are local filesystems. The repository contains no object-storage adapter, replication, backup scheduler, or shared-storage coordination.

Logging

Both binaries use the standard Go log package and write unstructured/prefix-tagged text to stdout/stderr. The supplied units send both streams to journald. Startup includes VCS build information when embedded by Go. qcarta emits detailed cache/tile diagnostics; QCARTA_CACHE_DEBUG=1 and default-enabled form parser logs can be noisy or contain request/filter/config data.