Skip to content

Mobile- und Native-App: Login & Sync

Zielgruppe: Entwickler einer Mobile-App (Flutter, Swift, Kotlin, React Native, Tauri-Desktop, …) oder eines Drittanbieter-Clients, der mit Prilog reden soll.

Status: Spezifikation gültig ab 2026-05-12 (JWT-only Login-Architektur).

Architektur-Überblick

Eine Prilog-Native-App spricht mit zwei Hosts:

  1. https://api.prilog.chat — Plattform-API (Login, Bootstrap, Berechtigungen, Modul-Daten).
  2. https://<tenant>.prilog.team — Matrix-Server für Realtime-Chat (Sync, Send, Media, Encryption).

Du brauchst zwei Tokens:

TokenWo wird er benutzt?Lebenszeit
prilog_jwtAlle /api/*-Aufrufe (Bootstrap, Module)24h
matrix_access_tokenAlle /_matrix/*-Aufrufe (matrix-js-sdk)bis Logout

Beide bekommst du aus einem einzigen POST /api/auth/v1/login-Call. Du musst NICHT separat gegen Synapse einloggen — externe /_matrix/client/v3/login wird mit 403 abgewiesen.

Login (initial)

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

{
  "tenant": "demo",                    // entweder slug oder volle matrix-domain
  "username": "anna",                  // local-part, ohne @ und ohne :domain
  "password": "<User-Passwort>",
  "deviceName": "Prilog iOS — Anna's iPhone",   // wird auf Synapse als display_name
  "issueRefreshToken": true            // empfohlen: true bei /login
}

Antwort 200:

json
{
  "token": "eyJhbGciOiJIUzI1NiI...",                          // prilog_jwt
  "expiresIn": 86400,                                         // Sekunden
  "matrixAccessToken": "syt_YW5uYQ_abcdef...",                // matrix_access_token
  "matrixUserId": "@anna:demo.prilog.team",
  "homeserver": "demo.prilog.team",
  "refreshToken": "9hQyD...",                                 // optional
  "refreshTokenExpiresAt": "2026-06-11T20:48:00.000Z"
}

Antwort 401 (User unbekannt, Passwort falsch, oder Konto deaktiviert):
Antwort 429 (Konto temporär gesperrt nach 10 Fehlversuchen — 15min Lock):
Antwort 503 (Synapse nicht erreichbar — Retry später):

json
{ "code": "INVALID_MATRIX_TOKEN", "message": "...", "retryable": false }

Token-Storage (App-Seite)

Speichere sicher (iOS Keychain, Android KeyStore, Tauri Secure Storage):

  • prilog_jwt + expiresIn (für Auto-Refresh-Logik)
  • matrix_access_token (Synapse-Token, kein expiresIn — bis explizites Logout gültig)
  • matrix_user_id
  • homeserver
  • refresh_token + refreshTokenExpiresAt

Refresh

Ein prilog_jwt läuft nach 24h ab. Statt User erneut nach Passwort zu fragen, nutze den Refresh-Token:

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

{ "refresh_token": "9hQyD..." }

Antwort 200:

json
{
  "token": "eyJ...",                          // neuer prilog_jwt
  "expiresIn": 86400,
  "refreshToken": "...",                      // neuer refresh_token (Rotation)
  "refreshTokenExpiresAt": "..."
}

Wichtig: Refresh-Tokens werden bei jeder Nutzung rotiert. Wenn ein bereits genutzter Token nochmal kommt, wird das Token-Family revoked (Replay-Attack-Schutz). Die App muss den neuen Token immer überschreiben.

Bei Refresh-Fehler (401, REFRESH_TOKEN_REUSED, etc.): User zurück zur Login-Page.

Matrix-Token braucht KEINEN Refresh — er bleibt gültig bis explizites Logout.

Logout

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

{ "refresh_token": "..." }   // revokes refresh-token, optional

→ 204 No Content. Lokale Tokens löschen, optional Synapse-/_matrix/client/v3/logout aufrufen (mit matrix_access_token).

Passwort ändern

http
POST https://api.prilog.chat/api/auth/v1/change-password
Authorization: Bearer <prilog_jwt>
Content-Type: application/json

{ "currentPassword": "alt", "newPassword": "neu-12345" }

→ 204 (Erfolg), 401 (currentPassword falsch), 429 (Locked), 400 (newPassword < 8 chars). Existierende Sessions bleiben gültig (Matrix-Tokens werden nicht invalidiert).

Matrix-Sync (Realtime-Chat)

Mit dem matrix_access_token sprichst du direkt mit Synapse:

http
GET https://demo.prilog.team/_matrix/client/v3/sync?since=...&timeout=30000
Authorization: Bearer <matrix_access_token>

Empfohlen: matrix-js-sdk (Web/React-Native) oder die offiziellen SDKs (Element-Android-SDK, MatrixSDK.swift, etc.). Setup-Beispiel mit matrix-js-sdk:

typescript
import { createClient } from 'matrix-js-sdk';

const client = createClient({
  baseUrl: `https://${homeserver}`,    // demo.prilog.team
  accessToken: matrixAccessToken,
  userId: matrixUserId,
  deviceId: undefined,                  // optional
});

await client.startClient({ initialSyncLimit: 30 });

Verfügbare Matrix-Endpoints unter https://<homeserver>/_matrix/*:

  • /_matrix/client/v3/sync (long-polling)
  • /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId} (Send)
  • /_matrix/media/v3/upload (Media)
  • /_matrix/client/v3/rooms/{roomId}/state (Read State)

Geblockt (HTTP 403 mit M_FORBIDDEN):

  • POST /_matrix/client/v3/login → Use /api/auth/v1/login instead
  • POST /_matrix/client/r0/login (gleicher Block, alle Versionen)

Plattform-API (Bootstrap, Module)

Alle Prilog-spezifischen Daten kommen via prilog_jwt:

http
GET https://api.prilog.chat/api/platform/v1/bootstrap
Authorization: Bearer <prilog_jwt>

Antwort enthält: Spaces, Aufgaben, Module, Berechtigungen, Profil. Detail-Endpoints unter /api/platform/v1/* und /api/customer/* (für Schul-Admin) — siehe Platform API Referenz.

Push-Notifications

Mobile-Apps können Synapse-Push-Gateways nutzen (Standard-Matrix-Spec):

http
POST https://demo.prilog.team/_matrix/client/v3/pushers/set
Authorization: Bearer <matrix_access_token>

Body folgt der Matrix-Pusher-Spec (FCM/APNs Push-Gateway-Config).

Tenant-Discovery

Wenn der User nur die Matrix-ID kennt (@anna:demo.prilog.team) und keine Tenant-URL, kann die App .well-known-Lookup machen:

http
GET https://demo.prilog.team/.well-known/matrix/client

{ "m.homeserver": { "base_url": "https://demo.prilog.team/" } }

Für Prilog-Login: extrahiere tenant aus dem Domain-Teil und nutze https://api.prilog.chat/api/auth/v1/login mit { tenant: "demo.prilog.team", username: "anna", ... }.

Mobile-spezifische Fallstricke

  • iOS Safari / WKWebView lokale Files: File-Refs verlieren Bytes wenn der Picker zwischenzeitlich schließt. Lies Files sofort in einen ArrayBuffer.
  • Refresh-Token-Race: Wenn die App zwei parallele Refresh-Aufrufe macht (z.B. Sync und Bootstrap gleichzeitig), wird der zweite revokes. Lösung: in der App Refresh serialisieren oder Mutex.
  • device_id ist optional im aktuellen Backend-Flow — wenn du End-to-End-Encryption nutzen willst, brauchst du jedoch eine stabile device_id. Setze sie explizit beim ersten Login per Custom-Endpoint (Backlog) oder nutze den Matrix-Standard-POST /devices.
  • Rate-Limiting: 10 Fehlversuche → Konto 15min gesperrt. Zeige dem User Countdown statt nur "401".

Beispiel-Sequenz (Pseudo-Code)

typescript
// 1. Initial-Login
const r = await fetch('https://api.prilog.chat/api/auth/v1/login', {
  method: 'POST', headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ tenant, username, password, deviceName: 'My App', issueRefreshToken: true }),
});
if (r.status === 401) showLoginError();
if (r.status === 429) showLockedError();
const { token, matrixAccessToken, matrixUserId, homeserver, refreshToken } = await r.json();
await SecureStore.set({ token, matrixAccessToken, matrixUserId, homeserver, refreshToken });

// 2. Bootstrap mit Prilog-JWT
const bootstrap = await fetch('https://api.prilog.chat/api/platform/v1/bootstrap', {
  headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());

// 3. Matrix-Sync starten
const matrixClient = createClient({ baseUrl: `https://${homeserver}`, accessToken: matrixAccessToken, userId: matrixUserId });
await matrixClient.startClient();

// 4. Token-Refresh wenn prilog_jwt nahe abgelaufen
async function refreshIfNeeded() {
  if (Date.now() > expiresAt - 60_000) {
    const r = await fetch('https://api.prilog.chat/api/auth/v1/refresh', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: refreshToken }),
    });
    if (!r.ok) logout();
    const next = await r.json();
    await SecureStore.set({ token: next.token, refreshToken: next.refreshToken });
  }
}

CORS / TLS

  • api.prilog.chat ist CORS-frei für *.prilog.team und für Capacitor/iOS/Android-Schemes.
  • Native-Apps brauchen keine spezielle Origin-Header-Konfiguration.
  • TLS-Pinning ist optional. Wer es nutzen will: api.prilog.chat läuft auf Let's-Encrypt-Standard, Cert dreht alle 90 Tage.

Migration: vorhandene Matrix-Clients aktualisieren

Wenn deine App bisher direkt POST /_matrix/client/v3/login aufruft (= alter Web-Client-Flow), funktioniert das nicht mehr seit 2026-05-12 — nginx blockt mit 403.

Umstellen:

AltNeu
POST <homeserver>/_matrix/client/v3/loginPOST api.prilog.chat/api/auth/v1/login
Antwort: { access_token, user_id, device_id }Antwort: { matrixAccessToken, matrixUserId, token }
POST <homeserver>/_matrix/client/v3/account/passwordPOST api.prilog.chat/api/auth/v1/change-password

Alle anderen /_matrix/*-Aufrufe (sync, send, media) bleiben unverändert.

Support & Issues

  • Backend: prilog-backend-api Repo
  • Connector: prilog-matrix-connector Repo
  • Bugs: GitHub Issues oder support@prilog.chat