Ü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
folderIdundvisibleToTenant 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:
{
"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:
{
"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:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Feld 'fileName' ist erforderlich",
"details": { ... }
}
}Wichtige Error-Codes, die die App behandeln sollte:
| HTTP | Code | Bedeutung |
|---|---|---|
| 401 | INVALID_PLATFORM_TOKEN | JWT abgelaufen → Re-Exchange noetig |
| 401 | INVALID_MATRIX_TOKEN | Matrix-Token ungueltig → Neu-Login |
| 403 | FORBIDDEN | Nutzer hat das Recht nicht — App zeigt ggf. Hinweis |
| 404 | NOT_FOUND | Resource entfernt oder nie da gewesen |
| 413 | STORAGE_LIMIT_EXCEEDED | Quota voll — Upload abbrechen |
| 503 | S3_NOT_CONFIGURED | Tenant 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/bootstrapLiefert: 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/:spaceIdListe 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=50folderId=__none__ filtert auf Root-Docs (kein Folder zugewiesen) — Finder-Pattern.
Antwort:
{
"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-SuggestionsGenau 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/downloadDatei-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-Historydownload 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/tagsFavoriten
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/:idtype = space | document | task | contact. Fuer die File-App relevant: document.
Prilog-Link aufloesen (prilog://file/<id>)
GET /api/platform/v1/files/:id/resolveWenn 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:
{
"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/confirmStern / 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-DeleteTags
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 zuweisenPhase 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/bulkVerteiler-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-InboxesSettings + 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 TagePhase 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/versionsDMS-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 — WiederherstellenDoc → 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/documentsDocument-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-broadcastsStufe 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/:docIdWebSocket 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-typeSaved Searches (gespeicherte Filter)
GET /api/platform/v1/saved-searches
POST /api/platform/v1/saved-searches
GET /api/platform/v1/saved-searches/:id/runAnnotationen (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/resolveDocument-Relations (Doc → Doc Verknuepfung)
GET /api/platform/v1/documents/:id/relations
POST /api/platform/v1/documents/:id/relationsPublic-Share-Links (Link extern teilen)
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 — AuditDie 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 wirdeIDAS-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/certificateAudioGuides (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 URLMobile-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: "..." }
→ 2042. 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/:idServer-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:
| Variable | Zweck |
|---|---|
APNS_KEY_PATH | Pfad zum .p8-File von Apple |
APNS_KEY_ID | 10-stellige Key-ID |
APNS_TEAM_ID | Apple-Team-ID |
APNS_TOPIC | Bundle-ID der App |
APNS_PRODUCTION | true fuer Production, sonst Sandbox |
FCM_SERVICE_ACCOUNT | Pfad 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 revokenPro 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=200Antwort:
{
"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| Size | Pixel |
|---|---|
| sm | 150×150 |
| md | 400×400 |
| lg | 1024×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 JPEGPlus 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:
{ "type": "connected", "subscriptionId": "..." }Danach werden alle Tenant-Events gepusht (gleiche wie SSE-Stream des Web-Clients):
{ "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
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).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.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.
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.
Wie geht die App mit dem Tenant-Branding um?
/bootstraplieferttenantBranding(Schul-Name, Farbe). App soll sich entsprechend einfaerben — sonst wirkt sie generisch.
Test-Konten & Sandbox
| Sandbox | Subdomain | Notes |
|---|---|---|
| Demo-Tenant | demo.prilog.team | Standard-Test-User: demo:demo. Voll funktionsfaehig, taeglich resetted. |
| Leander-Live | leander.prilog.team | Echter 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.