Skip to content

Übergabe: Native Mobile File-App

Zielgruppe: Entwickler, die eine native iOS/Android-App fuer den Datei-Zugriff auf prilog bauen. Stand 2026-05-05 (post-Folder-System). Backend-Version: prilog-backend-api main (Migration 0053 — DMS-Folder Phase A).

Neu seit letztem Stand (2026-05-05/06):

  • All-in-One-Login POST /api/auth/v1/login — ein Call statt Matrix + Exchange (empfohlen fuer Mobile)
  • 3-Stufen-Document-Visibility live: Tenant-Broadcast + Cross-Space-Share + Incoming-Shares
  • DMS-Folder-System (Google-Docs-Style) live: per-Space- und per-Mein-Fach-Folder mit Hierarchie, Soft-Delete, Audit
  • Document-Listing liefert folderId und visibleToTenant
  • folderId=__none__ als Marker fuer "Root-Docs only" (Finder-Style)
  • Collab-Editor-Endpoints fuer Live-Multiuser-Editing von Markdown (optional fuer Mobile-V1)

Zweck der App

Eine native Mobile-App, die einem prilog-Nutzer schnellen Zugriff auf seine Dateien gibt — Mein Fach (privates DMS), DMS-Dokumente in den Spaces, Volltextsuche, Tags, Up-/Download, Vorschau. Optional Postfach (Inbox) und Verteiler-Drops.

Was die App ausdruecklich nicht ist (in V1):

  • Kein Chat (Matrix-Client liegt im Web-Client und ist eigene Welt).
  • Kein Aufgaben-Manager (project-Module ist mobile-Web optimiert).
  • Kein Kalender (gleicher Grund).

Damit fokussiert die App sich auf den Use-Case "Ich brauch jetzt schnell die Datei vom Schreibtisch", weniger auf die volle Plattform-Flaeche.


Architektur-Ueberblick

┌──────────────────────────┐
│   Mobile App (iOS/Android)│
│                          │
│  - Auth-Modul            │
│  - Datei-Listen + Cache  │
│  - S3-Direct-Up/Download │
│  - Push-Notifications    │
└────────┬─────────────────┘
         │ HTTPS (Bearer-JWT)

┌──────────────────────────────────────┐
│  prilog-backend-api (api.prilog.chat)│
│                                      │
│  /api/auth/v1/*   — Matrix-Exchange  │
│  /api/platform/v1/* — Prilog-JWT     │
└────────┬─────────────────────────────┘

         ├──► Postgres (Metadaten)

         └──► Per-Tenant S3/MinIO (Datei-Bytes via presigned URLs)

Wichtigster Punkt fuer den App-Entwickler: Die App spricht nicht direkt mit S3-Bucket-Konfigurationen oder Synapse. Sie spricht ausschliesslich mit api.prilog.chat. Datei-Bytes werden ueber presigned URLs ausgeliefert, die der Backend pro Anfrage signiert.


Auth-Flow

prilog hat zwei Welten der Authentifizierung — Matrix (Chat-Identitaet) und Prilog-JWT (Plattform-Identitaet). Die App braucht beide.

Empfohlen fuer Mobile: der All-in-One-Login-Endpoint (siehe weiter unten) — ein Call statt zwei, bessere Fehlermeldungen, Refresh-Token automatisch.

Option A (empfohlen) — All-in-One-Login

POST https://api.prilog.chat/api/auth/v1/login
Content-Type: application/json

{
  "tenant": "leander",                        // oder "leander.prilog.team"
  "username": "max.muster",
  "password": "...",
  "deviceName": "Max iPhone 15",              // optional, taucht in Matrix-Devices auf
  "issueRefreshToken": true                   // optional, default: true
}

Antwort:

json
{
  "token": "eyJhbGciOi...",                   // Prilog-JWT
  "expiresIn": 86400,
  "matrixAccessToken": "syt_...",             // fuer optionale Matrix-Calls (Chat etc.)
  "matrixUserId": "@max.muster:leander.prilog.team",
  "homeserver": "leander.prilog.team",
  "refreshToken": "lAhX...",
  "refreshTokenExpiresAt": "2026-06-04T..."
}

Backend ruft intern Matrix /login und macht den Token-Exchange in einem Schritt. Bei falschem Passwort oder unbekanntem Tenant kommt ein generischer 401 (kein User-Existenz-Leak). Rate-Limiting laeuft ueber den globalen 5-Schichten-Layer.

Option B (Web-/Legacy-Flow) — zwei Schritte

Fuer Clients, die das Matrix-Token aus anderem Grund selbst halten (Web-Client tut das fuer Chat-SDK).

Schritt 1 — Matrix-Login

Nutzer gibt seinen prilog-Server (Subdomain) und Login/Passwort ein. Die App ruft den klassischen Matrix-Login-Endpoint auf:

POST https://<schule>.prilog.team/_matrix/client/v3/login
Content-Type: application/json

{
  "type": "m.login.password",
  "identifier": { "type": "m.id.user", "user": "<nutzername>" },
  "password": "<passwort>"
}

Antwort enthaelt access_token (Matrix-Token) und user_id (z. B. @max:weser.prilog.team).

Schritt 2 — Prilog-JWT-Exchange

POST https://api.prilog.chat/api/auth/v1/exchange
Content-Type: application/json

{
  "matrix_access_token": "<vom Schritt 1>",
  "homeserver": "weser.prilog.team"
}

Antwort:

json
{
  "platform_jwt": "eyJhbGciOi...",
  "expires_in": 86400,
  "user": { "matrixUserId": "@max:weser.prilog.team", "tenantId": "..." }
}

Der platform_jwt ist der Bearer-Token fuer alle weiteren API-Calls unter /api/platform/v1/*. Default-Lebensdauer ca. 24h — die App muss vor Ablauf neu via Schritt 2 erneuern (Matrix-Token laeuft viel laenger). Zum Erneuern braucht's nur den Matrix-Token, der wird vom OS sicher gespeichert (Keychain / Android-Keystore).

WARNING

Wichtig: Die App speichert beide Tokens niemals im Klartext im LocalStorage. OS-Keystore ist Pflicht. Beim Logout: beide Tokens loeschen plus Matrix-Logout (POST /_matrix/client/v3/logout).

Header fuer alle weiteren Calls

Authorization: Bearer <platform_jwt>

API-Konventionen

Alle Endpoints unter /api/platform/v1/* erwarten den Bearer-JWT, liefern und akzeptieren JSON, und antworten mit folgendem Error-Format:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Feld 'fileName' ist erforderlich",
    "details": { ... }
  }
}

Wichtige Error-Codes, die die App behandeln sollte:

HTTPCodeBedeutung
401INVALID_PLATFORM_TOKENJWT abgelaufen → Re-Exchange noetig
401INVALID_MATRIX_TOKENMatrix-Token ungueltig → Neu-Login
403FORBIDDENNutzer hat das Recht nicht — App zeigt ggf. Hinweis
404NOT_FOUNDResource entfernt oder nie da gewesen
413STORAGE_LIMIT_EXCEEDEDQuota voll — Upload abbrechen
503S3_NOT_CONFIGUREDTenant hat kein S3 — App zeigt "Datei-Speicher nicht eingerichtet"

Pagination: cursor-basiert. Listen-Endpoints liefern in der Antwort nextCursor (string|null). Naechste Seite: ?cursor=<wert>. Default limit 50, max 200.


Phase 1 — MVP-Endpoints (Read-Only)

Reicht fuer eine "ich schaue meine Dateien an"-App. Alle existieren schon im Backend.

Bootstrap (einmalig nach Login)

GET /api/platform/v1/bootstrap

Liefert: User-Profil, Tenant-Branding, aktive App-Module (featureFlags), aktiven Order. Die App ruft das direkt nach Login auf, cached die Antwort, refreshed bei Token-Erneuerung.

Spaces (Wo liegen meine Dokumente)

GET /api/platform/v1/spaces
GET /api/platform/v1/spaces/:spaceId

Liste aller Spaces, an denen der User beteiligt ist, plus Hierarchie- Information (parentSpaceId, name, color, memberCount).

Dokumente in einem Space

GET /api/platform/v1/spaces/:spaceId/documents
    ?q=<volltext>&tags=<slug,slug>&folderId=<id|__none__>
    &sort=<created|name|size>&order=<asc|desc>&cursor=<cursor>&limit=50

folderId=__none__ filtert auf Root-Docs (kein Folder zugewiesen) — Finder-Pattern.

Antwort:

json
{
  "documents": [{
    "id", "title", "mimeType", "sizeBytes", "starred",
    "folderId": "<dms_folder.id|null>",
    "visibleToTenant": false,
    "tags": [...], "uploadedBy", "createdAt", "updatedAt"
  }],
  "nextCursor": "..."
}

Liefert Docs aus diesem Space PLUS Docs die per Cross-Space-Share in den Space hineingeteilt wurden — Mobile sollte beide gleich behandeln, geteilte Docs erkennt man am abweichenden spaceId-Feld.

Cross-Space-Suche (alle Dokumente, an die der User darf)

GET /api/platform/v1/documents?q=<volltext>&tags=<slug>&spaceId=<id>&folderId=<id|__none__>&cursor=<cursor>
GET /api/platform/v1/documents/stats           — Zahl der Hits / Speicher
GET /api/platform/v1/documents/facets          — Tag-Counts fuer Filter
GET /api/platform/v1/documents/suggest?q=<x>   — Autocomplete-Suggestions

Genau das, was die globale Such-UI braucht. Listing-Response inkl. folderId und visibleToTenant.

Mein Fach (privates DMS)

GET /api/platform/v1/personal-fach/documents?q=<x>&tags=<slug>
GET /api/platform/v1/personal-fach/documents/:id
GET /api/platform/v1/personal-fach/documents/:id/download    — presigned URL
GET /api/platform/v1/personal-fach/tags
GET /api/platform/v1/personal-fach/quota                     — Speicher-Limit
GET /api/platform/v1/personal-fach/inbox                     — Postfach
GET /api/platform/v1/personal-fach/inbox/:id
GET /api/platform/v1/personal-fach/inbox/:id/download

Datei-Detail + Download

GET  /api/platform/v1/spaces/:spaceId/documents/:docId
GET  /api/platform/v1/spaces/:spaceId/documents/:docId/download   — { downloadUrl, fileName }
GET  /api/platform/v1/spaces/:spaceId/documents/:docId/preview    — bei Office/PDF
GET  /api/platform/v1/spaces/:spaceId/documents/:docId/versions   — Versions-History

download und preview antworten mit { downloadUrl: "<presigned-S3-URL>" }. Die App laed direkt von S3 — keine Bytes durchs Backend. URL ist 15 Min gueltig.

Tags

GET /api/platform/v1/tags

Favoriten

GET    /api/platform/v1/favorites
GET    /api/platform/v1/favorites/counts
POST   /api/platform/v1/favorites           — { type, refId }
POST   /api/platform/v1/favorites/toggle    — { type, refId }
DELETE /api/platform/v1/favorites/:id

type = space | document | task | contact. Fuer die File-App relevant: document.

GET /api/platform/v1/files/:id/resolve

Wenn die App per Deep-Link prilog://file/<id> aufgerufen wird, loest dieser Endpoint die ID auf den Space + Permission auf.


Phase 2 — Schreiben (Upload, Stern, Tag)

Upload (Direct-to-S3 — wichtiges Pattern!)

POST /api/platform/v1/spaces/:spaceId/documents/upload
Body: { fileName, mimeType, sizeBytes }

Antwort:

json
{
  "uploadUrl": "https://s3.example.com/...?X-Amz-Signature=...",
  "storageKey": "spaces/<sid>/uuid-<file>",
  "expiresAt": "2026-05-05T12:30:00Z"
}

Die App PUTted die Bytes direkt zu uploadUrl mit Content-Type: <mimeType> und Content-Length: <sizeBytes> — kein Bearer-JWT, kein anderer Header. Nach erfolgreichem PUT:

POST /api/platform/v1/spaces/:spaceId/documents/confirm-upload
Body: { storageKey, fileName, mimeType, sizeBytes, fileHash?, description?, tagIds? }

Erst dieser zweite Call legt den DB-Eintrag an. Wenn die App zwischen PUT und confirm-upload abstuerzt, liegt eine "verwaiste" Datei in S3 — wird vom Backend-Cleanup nach 24h aufgeraeumt.

Mein Fach Upload analog:

POST /api/platform/v1/personal-fach/documents/upload-url
POST /api/platform/v1/personal-fach/documents/confirm

Stern / Lock / Patch

POST   /api/platform/v1/spaces/:spaceId/documents/:docId/star    — togglet
POST   /api/platform/v1/spaces/:spaceId/documents/:docId/lock    — Owner-only
PATCH  /api/platform/v1/spaces/:spaceId/documents/:docId         — { title?, description?, tagIds? }
DELETE /api/platform/v1/spaces/:spaceId/documents/:docId         — Soft-Delete

Tags

GET   /api/platform/v1/tags
POST  /api/platform/v1/tags         — Tenant-Tag anlegen
POST  /api/platform/v1/personal-fach/documents/:id/tags  — Mein-Fach-Tag zuweisen

Phase 3 — Mein Fach + Inbox

Postfach-Aktionen

POST   /api/platform/v1/personal-fach/inbox/:id/archive
POST   /api/platform/v1/personal-fach/inbox/:id/move-to-docs
DELETE /api/platform/v1/personal-fach/inbox/:id
POST   /api/platform/v1/personal-fach/inbox/bulk

Verteiler-Drops (Datei an Personen schicken)

POST /api/platform/v1/personal-fach/drops/upload-url
POST /api/platform/v1/personal-fach/drops               — Liefert Datei in fremde Mein-Fach-Inboxes

Settings + Quota

GET  /api/platform/v1/personal-fach/settings   — Mein-Fach-Email-Alias, Auto-Archive
PUT  /api/platform/v1/personal-fach/settings
GET  /api/platform/v1/personal-fach/quota
GET  /api/platform/v1/personal-fach/audit      — Aktivitaeten der letzten 90 Tage

Phase 4 — Erweiterte Features (optional)

Versionen

POST /api/platform/v1/spaces/:spaceId/documents/:docId/versions          — neue Version anhochladen
POST /api/platform/v1/spaces/:spaceId/documents/:docId/versions/confirm
GET  /api/platform/v1/spaces/:spaceId/documents/:docId/versions

DMS-Folder (Google-Docs-Style, Phase 12)

Pro Space oder pro Mein-Fach-User eigene Folder-Hierarchien (max 7 Levels tief). Permissions ueber den Container — wer den Space sieht, sieht alle Folder darin.

POST   /api/platform/v1/dms-folders                              — Folder anlegen
       Body: { spaceId? | meinFach: true, parentId?, name }
GET    /api/platform/v1/spaces/:spaceId/dms-folders?parentId=X   — Listing (lazy)
GET    /api/platform/v1/personal-fach/dms-folders?parentId=X     — Mein-Fach-Folder
GET    /api/platform/v1/dms-folders/:id                          — Einzel-Folder
GET    /api/platform/v1/dms-folders/:id/path                     — Breadcrumb-Array
PATCH  /api/platform/v1/dms-folders/:id                          — Rename/Move/Watch
DELETE /api/platform/v1/dms-folders/:id                          — Soft-Delete (30d)
POST   /api/platform/v1/dms-folders/:id/restore                  — Wiederherstellen

Doc → Folder zuordnen:

POST   /api/platform/v1/documents/:id/move           Body: { folderId | null }
POST   /api/platform/v1/documents/move-batch         Body: { docIds[], folderId } (max 500)

Listing-Filter ?folderId= an /spaces/:id/documents und /documents:

  • ?folderId=<id> — nur Docs in diesem Folder
  • ?folderId=__none__ — nur Root-Docs (Finder-Style, ohne Folder-Zuordnung)
  • ohne Param: alle Docs der Sicht

Docs liefern folderId im Listing-Response.

Folder-Trees (Legacy, wird in Phase C entfernt)

Nur noch lesend nutzen, nicht produktiv beliefern. Migration zu DMS-Folder via Admin-Endpoint POST /api/admin/tenants/:id/migrate-folders.

GET  /api/platform/v1/folder-trees
GET  /api/platform/v1/folders/:id/documents

Document-Visibility (3-Stufen, Phase 11, LIVE)

Pro Dokument stehen drei Sichtbarkeits-Mechaniken zur Verfuegung:

Stufe 1 — Space-Default: Dokument ist nur fuer Space-Mitglieder sichtbar (Standardverhalten, kein Endpoint noetig).

Stufe 2 — Tenant-Broadcast ("schul-weit sichtbar"): nur Mitarbeiter mit Audience='staff' sehen das Doc dann unter /documents/tenant-broadcasts.

PATCH /api/platform/v1/documents/:id/visibility   Body: { visibleToTenant: bool }
GET   /api/platform/v1/documents/tenant-broadcasts

Stufe 3 — Cross-Space-Share: Doc gezielt in andere Spaces freigeben. Dokument bleibt im Source-Space, Empfaenger sehen es lesend mit "geteilt von …"-Badge.

POST   /api/platform/v1/documents/:id/space-shares      Body: { spaceId, note? }
GET    /api/platform/v1/documents/:id/space-shares      — alle Cross-Shares dieses Docs
DELETE /api/platform/v1/space-shares/:id                — Share zuruecknehmen
GET    /api/platform/v1/spaces/:spaceId/incoming-shares — was wurde mir reingeteilt?

Listing-Response liefert visibleToTenant: boolean und (im Cross-Share-Listing) sourceSpace.name mit, sodass Mobile die Badges rendern kann.

Collab-Editor fuer Markdown-Dokumente (Live-Multiuser, optional)

Markdown-Dokumente koennen kollaborativ via Y.js + Tiptap editiert werden. Mobile-V1 wird das vermutlich nicht implementieren (komplexer WebSocket + Y.js-Stack), die Endpoints sind aber da:

POST   /api/platform/v1/collab-docs/from-document/:documentId  — Editor-Session aus Doc
POST   /api/platform/v1/spaces/:id/collab-doc                  — neuer Draft
GET    /api/platform/v1/spaces/:id/collab-docs                 — eigene Drafts
POST   /api/platform/v1/collab-docs/:docId/save                — zurueck ins DMS schreiben
DELETE /api/platform/v1/collab-docs/:docId

WebSocket fuer Y.js-Sync: wss://api.prilog.chat/api/platform/v1/collab/:docId/ws?token=<jwt>.

Document-Types (Schul-spezifische Typen mit Custom-Fields)

GET   /api/platform/v1/document-types
PATCH /api/platform/v1/documents/:id/document-type

Saved Searches (gespeicherte Filter)

GET   /api/platform/v1/saved-searches
POST  /api/platform/v1/saved-searches
GET   /api/platform/v1/saved-searches/:id/run

Annotationen (Kommentare auf Seiten)

GET   /api/platform/v1/documents/:id/annotations
POST  /api/platform/v1/documents/:id/annotations
PATCH /api/platform/v1/annotations/:id
POST  /api/platform/v1/annotations/:id/resolve

Document-Relations (Doc → Doc Verknuepfung)

GET   /api/platform/v1/documents/:id/relations
POST  /api/platform/v1/documents/:id/relations
GET   /api/platform/v1/documents/:id/shares
POST  /api/platform/v1/documents/:id/shares          — Slug erzeugen, optional Passwort + Ablauf
POST  /api/platform/v1/shares/:id/revoke
GET   /api/platform/v1/shares/:id/views              — Audit

Die Public-Page selbst ist unauthentisiert:

GET  /api/public/shares/:slug             — Status (ok | needs_password | expired)
POST /api/public/shares/:slug/access      — { password? } → { downloadUrl }

Retention (Aufbewahrung)

GET   /api/platform/v1/retention-policies
PATCH /api/platform/v1/documents/:id/legal-hold
GET   /api/platform/v1/documents/expiring   — was demnaechst geloescht wird

eIDAS-Signature (Dokument signieren)

GET  /api/platform/v1/documents/:id/signature-requests
POST /api/platform/v1/documents/:id/signature-requests
GET  /api/platform/v1/signature-requests/:id/certificate

AudioGuides (mp3/Video mit Cue-Markern)

GET  /api/platform/v1/audio-guides              — Liste aller mit Cues
GET  /api/platform/v1/documents/:id/audio-guide — Cues + Meta
GET  /api/platform/v1/documents/:id/audio-guide/stream  — presigned URL

Mobile-Backend-Foundations (Stand 2026-05-05: LIVE)

Update vom 2026-05-05: Punkte 1-7 + 9 sind implementiert und unter api.prilog.chat verfuegbar (Migration 0050). Hier die fertigen Endpoints; Punkt 8 ist App-Seite und Punkt 2 hat Push-Provider-Stub (siehe Hinweis dort).

1. Refresh-Token-Pattern ✅ LIVE

Login-Flow-Erweiterung beim /exchange:

POST /api/auth/v1/exchange
Body: { matrix_access_token, homeserver, issue_refresh_token: true }

Antwort: {
  token: "<jwt>",
  expiresIn: 86400,
  refreshToken: "<48-byte-base64url>",
  refreshTokenExpiresAt: "2026-06-04T..."
}

Token-Erneuerung ohne Matrix-Login:

POST /api/auth/v1/refresh
Body: { refresh_token: "..." }

Antwort: { token, expiresIn, refreshToken (rotated), refreshTokenExpiresAt }

Reuse-Detection: ein bereits revoked Refresh-Token erneut zu nutzen revoked alle aktiven Tokens des Users (Schutz vor Token-Diebstahl).

Logout (idempotent):

POST /api/auth/v1/logout
Body: { refresh_token: "..." }
→ 204

2. Push-Notifications-Registrierung ✅ LIVE (mit Provider-Stub)

POST   /api/platform/v1/devices
  Body: { platform: "ios"|"android", pushToken, appVersion, deviceName?, osVersion? }
GET    /api/platform/v1/devices             — eigene Geraete-Liste
PATCH  /api/platform/v1/devices/:id         — { pushEnabled?, deviceName? }
DELETE /api/platform/v1/devices/:id

Server-seitig: pushToUser(tenantId, userId, payload) und pushToUsers(tenantId, userIds[], payload) in src/services/push-notification.service.ts.

WARNING

Aktueller Stand: APNs- und FCM-Versand sind als Stub implementiert. Der Service loggt was er gesendet haette, schickt aber noch keine echten Pushes. Der Real-Versand wird scharf, sobald folgende Env-Variablen gesetzt sind:

VariableZweck
APNS_KEY_PATHPfad zum .p8-File von Apple
APNS_KEY_ID10-stellige Key-ID
APNS_TEAM_IDApple-Team-ID
APNS_TOPICBundle-ID der App
APNS_PRODUCTIONtrue fuer Production, sonst Sandbox
FCM_SERVICE_ACCOUNTPfad zum FCM-Service-Account-JSON

Der Implementierungs-Code im Service ist als TODO mit Pseudo-Code vorbereitet — Switch von Stub auf echten Versand ist ein 1-Tag-Job sobald die Provider-Credentials da sind.

3. Sessions-Endpoints ✅ LIVE

GET    /api/platform/v1/sessions          — aktive Refresh-Tokens (Liste)
DELETE /api/platform/v1/sessions/:id      — einzelne Session beenden
POST   /api/platform/v1/sessions/revoke-all — alle Sessions revoken

Pro Session: createdAt, lastUsedAt, expiresAt, userAgent, ipAddress. Web-Sessions tauchen nicht auf (kein Refresh-Token), nur Mobile.

4. Delta-Sync-Endpoints ✅ LIVE

GET /api/platform/v1/spaces/:spaceId/documents/changes?since=<iso>&limit=200
GET /api/platform/v1/personal-fach/documents/changes?since=<iso>&limit=200
GET /api/platform/v1/documents/changes?since=<iso>&limit=200

Antwort:

json
{
  "added":    [Document, ...],
  "modified": [Document, ...],
  "deleted":  [{ "id": "..." }, ...],
  "syncedUntil": "2026-05-05T13:45:00Z"
}

App speichert syncedUntil lokal und uebergibt es als since beim naechsten Aufruf. Limit 500 pro Liste — bei mehr Aenderungen mehrmals mit aelterem since aufrufen.

5. Mobile-Thumbnails ✅ LIVE

GET /api/platform/v1/spaces/:spaceId/documents/:docId/thumbnail?size=sm|md|lg
GET /api/platform/v1/personal-fach/documents/:docId/thumbnail?size=sm|md|lg
SizePixel
sm150×150
md400×400
lg1024×1024

Antwort: HTTP 302 → presigned S3-URL (15 Min gueltig). Lazy Render via sharp, gecached in S3 unter <storageKey>.thumb-<size>.jpg. Nur fuer mimeType: image/*. EXIF-Orientierung wird respektiert.

6. PDF-Page-Images ✅ LIVE

GET /api/platform/v1/spaces/:spaceId/documents/:docId/pages
  → { totalPages, pages: [{ pageNumber, imageUrl }] }

GET /api/platform/v1/spaces/:spaceId/documents/:docId/pages/:n/image
  → 302 → presigned JPEG

Plus Mein-Fach-Pendant. Lazy Render via pdf-img-convert + sharp, gecached in S3 unter <storageKey>.page-<n>.jpg. Nur fuer mimeType: application/pdf. Erste Seite kann ein paar Sekunden brauchen (PDFjs-Init), nachfolgende Seiten kommen schneller.

7. App-Version-Check ✅ LIVE (kein Auth)

GET /api/platform/v1/mobile/version-check?platform=ios|android&app_version=x.y.z&tenant_id=optional

Antwort:
{
  "minSupportedVersion": "1.0.0",
  "latestVersion":       "1.2.3",
  "forceUpdate":         false,
  "storeUrl":            "https://apps.apple.com/..."
}

Pro Tenant koennen Admins eigene Policies setzen (mobile_app_version_policies), sonst gilt der globale Default. forceUpdate=true wenn app_version < minSupportedVersion.

8. File-Picker Provider (iOS Files / Android SAF) → App-Seite

Nicht backend-seitig — die App registriert sich auf iOS via UIDocumentPickerViewController und auf Android via SAF (Storage- Access-Framework) als File-Provider. Backend ist nicht beteiligt.

9. Realtime-WebSocket ✅ LIVE

WS wss://api.prilog.chat/api/platform/v1/realtime?token=<jwt>

Auth via Query-Parameter (WebSocket unterstuetzt keine Authorization-Header). Nach Connect bekommt der Client:

json
{ "type": "connected", "subscriptionId": "..." }

Danach werden alle Tenant-Events gepusht (gleiche wie SSE-Stream des Web-Clients):

json
{ "type": "event", "name": "document.changed", "data": { spaceId, documentId, action } }
{ "type": "event", "name": "calendar.changed", "data": { ... } }
{ "type": "event", "name": "task.changed",     "data": { ... } }
{ "type": "event", "name": "run.updated",      "data": { ... } }

Heartbeat: Server sendet ping alle 30s, Client soll mit pong antworten (oder Browser/native macht das automatisch). Nach 60s ohne Pong wird die Verbindung getrennt.

TIP

Empfehlung: WebSocket erst zuschalten, wenn Push-Notifications nicht reichen (Push triggert App-Open + Pull-Refresh — fuer "sofort sichtbar im offenen Tab" braucht's WS). Native Apps koennen beides parallel: Push fuer Wakeup, WS waehrend Foreground.


Empfohlener Build-Plan fuer die App-Devs

Realistische Reihenfolge fuer einen 4-6-Monate-Projekt mit Shippable-MVP nach 6 Wochen:

Sprint 1-2 (4 Wochen) — MVP

  • Backend-Erweiterung 1+5+7 (Refresh-Tokens, Thumbnails, Version-Check).
  • App: Login (Matrix → JWT-Exchange), Bootstrap, Spaces-Liste, Datei-Liste pro Space, Volltextsuche, Download via presigned URL, Mein Fach + Inbox lesen.
  • Funktional minimal, aber alle "ich brauch jetzt schnell die Datei"- Wege da.

Sprint 3 (2 Wochen) — Schreiben + Push

  • Backend-Erweiterung 2 (Push-Registration + APNs/FCM-Setup).
  • App: Upload (Direct-to-S3), Stern, Tag, Inbox-Move-to-Docs, Push-Receive fuer Inbox-Drops.

Sprint 4 (2 Wochen) — Offline + Stabilitaet

  • Backend-Erweiterung 4 (Delta-Sync-Endpoints).
  • App: lokale SQLite-DB fuer Doc-Liste, Offline-Lese-Cache der letzten 100 Dateien, Sync-on-Foreground.

Sprint 5 (2 Wochen) — Erweitert + Polish

  • Backend-Erweiterung 6 (PDF-Page-Images).
  • App: PDF-Viewer mit Page-Navigation, Versionen-History, Public- Share-Erstellung, Settings-Screen.

Sprint 6 (2 Wochen) — Beta + Polish

  • TestFlight + Play-Beta.
  • Backend-Erweiterung 8 wenn User es einfordern.
  • Privacy-Policy, AGB-Akzeptanz, Daten-Export.

Konkrete Punkte, die das App-Team auf jeden Fall klaeren muss

  1. Wo speichert die App heruntergeladene Dateien lokal? iOS: App-Sandbox /Library/Caches/ (vom System geloescht bei Speicher-Druck) vs. Documents/ (in iCloud-Backup). Android: getCacheDir() vs. getFilesDir() vs. SAF. Empfehlung: Caches-Dir mit einer eigenen LRU-Liste (max 1 GB, AeltestE zuerst loeschen).

  2. Wie wird ein Datei-Konflikt aufgeloest? User editiert Datei nativ (z. B. PDF mit Annotations) — nach Re-Upload soll das eine neue Version werden, nicht ueberschreiben. Server hat dafuer den /versions-Endpoint, App muss das nutzen.

  3. Wie viele Dateien max gleichzeitig offline? Architektur-Entscheidung: ALLE Mein-Fach-Dateien (typ. < 5 GB) vs. nur favoritisierte vs. nur explizit "fuer offline markiert". Fuer die App-UX wichtig.

  4. Welche Foto-Upload-Pipeline? Beim Capture aus der Kamera: direkt nach Mein-Fach hochladen, oder in lokalen "Outbox"-Bereich, der erst bei WLAN syncs? Ich rate zu letzterem mit Progress-UI.

  5. Wie geht die App mit dem Tenant-Branding um?/bootstrap liefert tenantBranding (Schul-Name, Farbe). App soll sich entsprechend einfaerben — sonst wirkt sie generisch.


Test-Konten & Sandbox

SandboxSubdomainNotes
Demo-Tenantdemo.prilog.teamStandard-Test-User: demo:demo. Voll funktionsfaehig, taeglich resetted.
Leander-Liveleander.prilog.teamEchter Tenant, NICHT zum Testen — nur fuer App-Demo verwenden.

API-Base-URL fuer alle Tenants: https://api.prilog.chat/api.


Ansprechpartner

  • Backend-API-Themen: Lee (Anthropic / brasilspace) — direkt im Slack-Kanal #prilog-backend.
  • Datenmodell-Fragen: prisma-Schema unter prilog-backend-api/prisma/schema.prisma — aktuell 49 Migrationen.
  • Bug-Reports: GitHub-Issue-Tracker im Repo, oder direkt im Slack.

Stand der Doku-Migration

Diese Datei ist die Quelle der Wahrheit fuer App-Entwickler. Aenderungen am API werden hier aktualisiert. Versionierung der API ueber URL-Prefix (/api/auth/v1, /api/platform/v1) — Breaking-Changes bekommen /v2.