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/impersonate |
Sysadmin only: act as a concrete org admin (tenant_id, optional user_id). Session becomes that user; original_user_id keeps the sysadmin for restore. user_id required when the org has multiple admins |
| DELETE | /auth/impersonate |
End impersonation; restore the sysadmin session |
| 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 a single system admin from LINETRA_BOOTSTRAP_ADMIN_* (no organization). Organizations and their first org admin are created later via POST /tenants (sysadmin only). 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). Rows carry project_count (project memberships); the org-wide communication_role is gone — communication follows the project role (ADR-0069) |
| 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/me/qgis-client-bundle |
Own mTLS client-cert ZIP (reuse existing); QGIS plugin fallback |
| POST | /users/{id}/qgis-client-bundle |
Self: own ZIP with reuse; org admin: issue fresh ZIP for another user. CN = personal LOGIN {slug}_u_{hex8} (ADR-0055) |
| POST | /users/{id}/telegram-invite |
Personal Telegram invite link {url, expires_at}, valid 7 days; 400 until a Telegram bot is connected and tested (ADR-0068). List rows carry telegram_linked. |
| DELETE | /users/{id}/telegram |
Unlink the employee's Telegram chat (204) |
Auth (self-service)
| Method | Path | Notes |
|---|---|---|
| POST | /auth/me/qgis-client-bundle |
Signed-in user downloads own QGIS cert ZIP (reuse; same personal CN) |
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 of tenant DB (pg_dump + control metadata + project files). Refuses when catalog dbname is the control DB |
| DELETE | /tenants/{id} |
System admin: hard-delete after { confirm_slug }. Drops only linetra_<slug> (never control). Bootstrap org and LINETRA_SHARED_DB refuse. Legacy control-dbname rows: catalog only |
POST /projects (create) requires can_create_project (org admin); the caller becomes Leitung (lead). Project rights are the intersection of the org right edit_records and the project role (ADR-0069, app/projects/access.py):
| Right | lead |
office |
field |
|---|---|---|---|
| See the project | ✓ | ✓ | ✓ |
Manage members, project settings (briefing_time, assistant_policy), delete project |
✓ | – | – |
| GIS / records (QGIS edit), Betrieb approvals, assign tasks, project documents | ✓ | ✓ | – |
A guest (customer, no edit_records) stays read-only whatever the project role. Org admins see and manage every project without a membership (acting as lead); staff and guests only see projects they are members of. There are no unclaimed projects to take over any more.
Stammdaten
Paginated CRUD:
/organizations/persons(Filter über Mitgliedschaften; Responseorganization_ids)/projects(membership-scoped, org admins see all; flagsmy_role=lead|office|field| null,can_edit_gis,can_edit_project,can_manage_members,can_delete_project)GET/POST /projects/{id}/members,PATCH/DELETE /projects/{id}/members/{membership_id},GET /projects/{id}/eligible-users.POSTtakes{ user_id, role }(defaultfield); any employee of the organization qualifies, with or without web login. Members carryuser_id,person_id,display_name,username,has_account,phone,messenger,role. One membership per person and per user; severalleads allowed; only an org admin can remove or demote the lastlead. Managing members needsleador org admin./projects/{id}/contacts(field contacts,project_contacts) is removed; field workers are members with rolefield./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). Optional display_pattern composes record labels from fields (ADR-0058) |
| 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. Layers prepend immutable QGIS system rows (origin=system: object_id, project_id, label, status, geom, created_at, updated_at) — not stored in field_definitions (ADR-0056). Reserved system codes and system field_ids cannot be created/updated/deleted. instantiation_scope Global (base) stays on standard; pack-local (project) needs schema_code of that named schema. Every field has cardinality (n or 1–99) and required (ADR-0039). Optional form_group_code assigns a layout form group (ADR-0057). Enum/list require list_code unless list + item_type: group. Relation requires config.target_kind (existing entity kind, not a group) |
| GET/POST | /entity-kinds/{code}/form-groups |
List / create layout-only form groups for web forms (ADR-0057); ?working=true when checkout holder |
| PUT/DELETE | /entity-kinds/{code}/form-groups/{groupCode} |
Update label/sort_order or delete (fields keep values; form_group_code cleared) |
| 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). q matches search_text (including the display-pattern label) and the record id. Federated data objects always use the shared pool (project_id NULL); project_id is allowed for any global Standard kind, otherwise the project must bind the kind (403). label uses the display pattern or a name-field fallback (ADR-0058) |
| 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 role lead / office (with org edit_records), false for field. Membership (or org admin) required when project_id is set (ADR-0052, ADR-0069). 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=§ion_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 |
Operations (Betrieb)
Full route list and messaging flow: Prozessmanagement. Betrieb works per project (ADR-0069):
| Method | Path | Notes |
|---|---|---|
| GET | /operations/inbox, /operations/inbox/messages, /operations/review, /operations/agent-runs |
project_id required; project role lead or office (or org admin) |
| GET | /operations/reports |
project_id required; any member (or org admin) |
| GET | /operations/status, /operations/tasks/summary, /operations/alerts/summary |
Accessible projects only; tasks/summary includes project_name; alerts/summary takes an optional project_id |
| GET | /operations/projects/{id}/issues |
Issue mirror; assignee_name and assignee_is_member (false when the assignee is not on the project) |
| POST | /operations/review/{id}/approve etc. |
Project role lead or office |
Documents
Dokumentenverwaltung, Rechte und Filter: Dokumente (Technik).
| Method | Path | Notes |
|---|---|---|
| GET/POST | /documents |
Liste mit Filtern und sort; POST = Multipart-Upload (413 zu groß, 415 unzulässiger Typ) |
| GET/PATCH | /documents/{id} |
Detail, Metadaten |
| POST | /documents/{id}/file |
Bestätigen & ablegen |
| POST | /documents/{id}/reject |
Ablehnen |
| POST | /documents/{id}/copy |
Als eigenständiges Dokument kopieren (201) |
| POST | /documents/{id}/trash, /documents/{id}/restore |
Papierkorb / Wiederherstellen (documents.delete) |
| POST | /documents/{id}/versions |
Neue Fassung |
| GET | /documents/{id}/content |
Inhalt (?version_id, ?download) |
| GET | /documents/{id}/pages/{n} |
PNG-Vorschau einer PDF-Seite (1–20, ?version_id) |
| GET | /documents/{id}/events |
Verlauf |
| POST/DELETE | /documents/{id}/links, /documents/{id}/links/{link_id} |
Verknüpfungen |
| GET | /documents/catalog, /documents/inbox/summary |
Typkatalog, Badge-Zähler |
| GET/PUT | /documents/settings |
Einstellungen (manage_org_settings) |
| GET | /projects/{id}/documents/tree |
Ablagebaum |
| GET | /projects/{id}/documents/export |
?format=csv\|json |
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, pdf_oxide, document_storage |
OpenAPI: /docs and /openapi.json on the API process (inside Compose, typically via backend:8000). Hosted product docs: Next.js /documentation/ → Compose docs.