Skip to content

API (FastAPI)

Base path: /api/v1. The Next.js app proxies /api/* to the backend service (same host cookies).

Auth

Web login uses rows in app_users on the control database linetra. Fachdaten live in linetra_<slug>. Postgres roles {slug}_edit / {slug}_read are the QGIS/API engines for that tenant (ADR-0031).

Method Path Notes
POST /auth/login Sets httpOnly session + CSRF cookie
POST /auth/logout Clears cookies
GET /auth/me Principal including tenant_id, can_create_project, and qgis_connection (host, port, dbname, role, sslmode, account_name / account_slug; password on Bearer including mTLS so the plugin can set the engine LOGIN; cookie sessions omit the password)
POST /auth/token Editor-Bearer (Keychain); kein Cookie
PATCH /auth/me Change own username (current password required)
POST /auth/me/password Change own password (current password required)

Mutating requests (except login/logout/token) require header X-CSRF-Token matching the CSRF cookie. Editor Bearer requests skip CSRF.

Error bodies are { "error": "<message>", "code": "<code>" }. Unexpected failures (including missing DB columns) return 500 server_error or 503 database_error with a generic message — they are not 401s. Failed login credentials return 401 invalid_credentials.

On empty app_users, startup seeds accounts from LINETRA_BOOTSTRAP_ADMIN_* (and optional LINETRA_BOOTSTRAP_EDITOR_*). Postgres roles LINETRA_APP_USER / LINETRA_READONLY_USER are DB engines only.

Users (org admin)

Method Path Notes
GET /users Staff of the caller's organization (platform operator with tenant_id null: all)
POST /users Invite into the caller's organization (role admin/editor/reader, …)
PATCH /users/{id} Role, is_active, optional password reset
DELETE /users/{id} Permanent delete; not self; cannot remove last active org admin. Deactivate via PATCH { is_active: false }.
POST /users/{id}/qgis-client-bundle Issue mTLS client cert ZIP; CN = Org-DB-Rolle; VERBINDUNG.txt names linetra_<slug>

Organization accounts (tenants)

Method Path Notes
GET /tenants/me Bound organization
PATCH /tenants/me Rename own organization (org admin or sysadmin)
GET /tenants Own organization, or full catalog for system admin (can_manage_tenants)
POST /tenants System admin only: provision linetra_<slug> + {slug}_edit/{slug}_read and first org admin in the body
PATCH /tenants/{id} System admin: rename any organization
GET /tenants/{id}/backup System admin: ZIP (pg_dump + control metadata + project files); header X-Deletion-Token (1h) unlocks delete
DELETE /tenants/{id} System admin: hard-delete after { confirm_slug, deletion_token }. Bootstrap org and LINETRA_SHARED_DB refuse.

POST /projects (create) requires can_create_project (org admin); the caller becomes owner. DELETE /projects/{id} requires owner (or org admin on an unclaimed legacy project). Editors keep org can_edit for Stamm; project updates and GIS need project role owner or editor. List and get are membership-scoped (ADR-0052).

Stammdaten

Paginated CRUD:

  • /organizations
  • /persons (Filter über Mitgliedschaften; Response organization_ids)
  • /projects (membership-scoped; flags my_role, can_edit_gis, can_edit_project, can_manage_members, can_delete_project. Org admins also see projects with no members yet)
  • GET/POST /projects/{id}/members, PATCH/DELETE /projects/{id}/members/{membership_id}, GET /projects/{id}/eligible-users
  • /lookups/project-status
  • /actors (paginated union: kind = person | company | municipality, q)

Schema-CMS (Management)

Flexible entity kinds with hybrid QGIS compat (ADR-0025, ADR-0033):

Method Path Notes
GET/POST /catalog-checkout Exclusive schema session. GET returns holder, is_dirty, is_holder, can_unlock, can_force. Parked dirty lock looks locked (is_holder=false) and keeps the overlay. GET /diff (holder): kinds/fields added/removed/changed with before/after. POST /unlock (resumes a parked draft), /apply ({ confirm: true }), /discard, /lock (parks dirty overlay), /force-lock (org admin). Schema writes 409 without an unparked holder (ADR-0046)
GET/POST /schema-definitions Named schemas with projects[] (id + name). Without ?project_id= the list is standard first, then every custom pack; with it, only that project's packs plus standard. Create is always project-bound (is_federated=true rejected) (ADR-0037). Holder reads pass ?working=1
GET/PUT/DELETE /schema-definitions/{code} PUT renames a custom pack (label). Standard (standard) cannot be renamed or deleted
GET /schema-definitions/{code}/er Compact ER graph: kinds as nodes (entity_type), relation/parent edges with cardinality and required. Holder overlay: ?working=1
GET/POST /entity-kinds List/create; optional ?entity_type=dataset\|group\|layer; ?schema_code=; holder overlay ?working=1. Writes 409 without checkout. instantiation_scope Global (base) always lands on standard; optional parent_code (ADR-0045): group→group (nest), layer→layer or dataset→dataset (one inherit step, live fields, parent abstract). Leaf layers list groups in group_codes (ADR-0048); inherit parents cannot. Layer requires geom_kind (point/linestring/polygon). Groups have no fields. If the parent has records or GIS features, parent_instances=transfer\|delete is required. is_federated only on standard datasets (ADR-0043)
POST /entity-kinds/{code}/promote Create a layer child under a dataset (inherits fields, upserts geo_object_kinds when the layer is a leaf); source becomes abstract; same parent_instances choice
GET/POST /selection-lists Named pick lists (code, label, entries[]) for enum/list fields (ADR-0042)
GET/PUT/DELETE /selection-lists/{code} One list. Delete is 409 while a field still binds list_code
GET/POST /entity-kinds/{code}/fields List (incl. inherited, optional ?project_id= / ?schema_code=) / create field. instantiation_scope Global (base) stays on standard; pack-local (project) needs schema_code of that named schema. Every field has cardinality (n or 199) and required (ADR-0039). Enum/list require list_code unless list + item_type: group. Relation requires config.target_kind (existing entity kind, not a group)
GET/PUT/DELETE /entity-kinds/{code} Seeded kinds deletable (QGIS warning). DELETE ?retain_in_projects=true demotes a global kind so existing project schemas keep it (ADR-0038)
PUT/DELETE /entity-kinds/{code}/fields/{field_id} Update/delete field. DELETE ?retain_in_projects=true keeps a global field on existing project schemas
GET/POST /entities?kind= Paginated records (q, project_id, scope=central\|project). Federated data objects always use the shared pool (project_id NULL); project_id then requires a schema binding (403 otherwise)
GET/PUT/DELETE /entities/{record_id} One record
POST/GET /entity-files Upload / download image or document
GET /catalog/{kind} Central records only (project_id null), paginated
POST /projects/{id}/catalog/pull Copy selected central records (max 100)
POST /projects/{id}/sync Catalog sync (direction, kinds); Celery or eager
GET /projects/{id}/sync/conflicts Open conflict inbox
POST /projects/{id}/sync/conflicts/{id}/resolve { winner: central\|project }
POST /projects/{id}/schema/sync-from-catalog Additive base bindings
POST /projects Always creates a named project schema. clone_schema_from copies bindings from that project; omit for Standard (ADR-0047)
GET /analyse/summary?project_id= Counts by layer kind (no geometries)
GET /analyse/overview?project_id= Dashboard KPIs: geo_object counts by kind/status, schema number-field sums, procedure status + open drafts
GET /analyse/field?project_id=&kind=&field= Bounded JSONB histogram
GET /analyse/catalog?project_id=&kind= Central vs project-copy counts

| GET | /qgis/layers?project_id= | Plugin apply: groups + leaf-layer views. can_edit is true for project owner/editor, false for reader. Membership required when project_id is set (ADR-0052). Omit project_id for the whole published catalog with can_edit=false (ADR-0049) |

Default seed kinds sync to projects / persons / organizations / geo_object_kinds. Leaf layers also project qgis_<kind> views.

GIS import (planner → agent)

Method Path Notes
POST /projects/{id}/import/upload Multipart .gpkg / GeoJSON / Shapefile .zip
POST /projects/{id}/import/inspect { file_id } — GDAL layer/field/CRS summary
GET /projects/{id}/import/files/{file_id} Upload + cached inspect
GET/POST/PUT/DELETE /projects/{id}/import/profiles Per-project mapping profiles
POST /projects/{id}/import/run Mapped import → geo_objects (Celery); no pipeline stage

Compat: POST /map/import still accepts inline GeoJSON for simple imports (ADR-0026).

Mutations require admin or editor. projects.project_manager stores persons.person_id as text (QGIS Value Relation). organizations.contact_person_id on write sets primary membership, not a separate FK column.

Reports

Method Path Notes
GET /reports/summary Chart aggregates
GET /reports/export.pdf PDF download

Map

Hintergrund: MVT. Identify: GeoJSON eines Objekts. Geometrie-POST/PATCH nur Editor (Bearer). Dashboard ändert Fachfelder über /map/objects/{id}/attributes.

Method Path Notes
GET /map/tiles/{stage}/{z}/{x}/{y}.mvt?project_id=&kind= Auth: session cookie or editor Bearer. {stage} is ignored (kept for old clients). Filter in 25832, clip 3857
GET /map/layer-tree?project_id= Schema explorer: groups + leaf layers with counts, plus extent_3857 (padded Web-Mercator box, or null if empty). No geometries in the tree. No pipeline stages / draft / versions (ADR-0045)
GET /map/layer-colors Hex catalog { stages: {}, kinds } — kind swatches only
PUT /map/layer-colors/{kind}/{code} Set one kind swatch (#RRGGBB); editor/admin. Scope stage is rejected
GET /map/objects?project_id=&kind=&section_id=&layer_id= Paginated feature list (no geometry). stage / version_id unused leftovers
GET /map/objects.geojson?project_id=&kinds= Layer export: FeatureCollection EPSG:25832, max 10 000; optional comma-separated kinds
GET/POST/PUT/DELETE /map/kinds GIS product-kind registry (geo_object_kinds)
GET /map/objects/{id} GeoJSON + attrs + Beilage
PATCH /map/objects/{id}/attributes Dashboard: label + Beilage only (admin/editor cookie)
POST /map/project-versions 410 — pipeline removed (ADR-0045)
PATCH /map/project-versions/{version_id} 410
POST /map/objects/copy-to-normierung 410
POST/PATCH /map/objects Editor only (geometry required). stage / version_id ignored (always null)
DELETE /map/objects/{id} Editor only; detaches leftover version pins first

Collaboration / reports / export

Method Path Notes
GET/POST /projects/{id}/chat Kunde ↔ Betreiber
GET/POST /projects/{id}/tickets Optional jira_key, kein Jira-Sync
GET/POST /product-reports/sets Feld-Sets (Admin schreibt)
POST /exports/ltp/{project_id} Celery .ltp (Magic LINETRA-PP\\0)
POST /exports/ltp/{project_id}/import Roundtrip-Import der aktuellen Objekte
GET /exports/ltp/{project_id} Download des letzten .ltp

Procedures (Verfahren)

Pagination auf Listen. Behörden-Filter räumlich (ST_Intersects). GIS-Write nicht in diesem Namespace.

Method Path Notes
GET /kind-packs Pack-Registry inkl. Mitglieds-Kinds
GET/PUT /projects/{id}/kind-packs Aktivierte Vertikale des Projekts
GET/POST /pipeline/sections work_sections; POST darf geom (GeoJSON 25832) oder Linearref
PATCH/DELETE /pipeline/sections/{section_id} Umbenennen; DELETE mit ?delete_contents=true löscht Features, sonst section_id auf NULL
GET /procedures Filter: project_id, status, q; Pagination
POST /procedures Antrag auf Abschnitt (draft)
GET /procedures/{id} Antrag + Auflagen + letzte Events
POST /procedures/{id}/transitions Body { to_status }; 409 wenn illegal
GET/POST /procedures/{id}/conditions Auflagen
GET /procedures/{id}/events Audit (letzte N, default 50)
GET /procedures/{id}/events.csv Audit-Export; ?project_id= auf Collection-Export
GET /notifications In-App-Inbox
GET /field/sections nur approved / approved_with_conditions im Gebiet (foreman/admin)
POST /field/photos Foto in File-Store, Query procedure_id
POST /field/reports Fortschritt + optionale file_id

Logs

In-process ring buffer of structured JSON events from this API process (not Celery or Next.js). Capacity and default level come from LOG_BUFFER_SIZE / LOG_LEVEL (or DEBUG=true). Org admins and system admins only.

Method Path Notes
GET /logs Paginated; page, page_size, min_level, levels (comma-separated), q, sort (timestamp_desc default, timestamp_asc, level_desc, level_asc)
GET /logs/config Live process level, env default, buffer_size, captured
PATCH /logs/config { log_level } runtime overlay — resets on process restart

Secret-like extra keys (password, token, …) are redacted as [redacted].

Health

Method Path Notes
GET /health Liveness
GET /health/dependencies Postgres, Redis, Celery, Docs

OpenAPI: /docs and /openapi.json on the API process (inside Compose, typically via backend:8000). Hosted product docs: Next.js /documentation/ → Compose docs.