diff --git a/docs/architecture-modes.md b/docs/architecture-modes.md index 31a30cb..ffe3b4c 100644 --- a/docs/architecture-modes.md +++ b/docs/architecture-modes.md @@ -5,60 +5,41 @@ systems from the same backend catalog; the difference is **who signs the user in and **where the data ends up**. No school system is hardcoded in the app - every one of them comes from the catalog. -```mermaid -flowchart TB - User(["User"]) - User --> Gate{"App mode?"} - - Gate -->|"Account"| Login - Gate -->|"Private (no login)"| Catalog +Both modes end up reading the same school system. What differs is where your login is +kept and whether any of the data comes to rest on a server. - subgraph ACCOUNT["Account mode - Schuly login"] - direction TB - Login["OIDC login
(Keycloak)"] - ApiClient["ApiClient
Bearer token + auto-refresh"] - Backend[("SchulyBackend
authenticated /api/*")] - DB[("PostgreSQL
data stored per user")] - Sync["Plugin sync tasks
(background, recurring)"] - Login --> ApiClient --> Backend - Backend <--> DB - Sync -->|"stores"| DB - end - - subgraph PRIVATE["Private / secure mode - NO login, NO OIDC"] +```mermaid +flowchart LR + subgraph account["Account mode"] direction TB - Catalog["SchoolSystemsService
clean Dio (no auth interceptor)"] - AnonCat[("GET /api/app/school-systems
[AllowAnonymous]")] - Connect["Generic connect screen
renders loginFields, branches on privateAuthStrategy"] - TP["TokenProxyClient
clean Dio"] - SP["ScrapeProxyClient
clean Dio"] - Stateless[("Backend stateless proxy
/api/plugins/*/stateless/*
[AllowAnonymous] - stores nothing")] - Keystore[("On-device keystore only")] - Auth["Authenticator screen
on-device TOTP from vaulted seed"] - Catalog --> AnonCat - Catalog --> Connect - Connect -->|"token"| TP - Connect -->|"scrape"| SP - TP --> Stateless - SP --> Stateless - TP -.->|"token + context + email/password/TOTP seed saved"| Keystore - SP -.->|"creds saved"| Keystore - Keystore -.->|"seed"| Auth - Keystore -.->|"silent re-login on expiry"| TP + AU(["You"]) -->|"sign in to Schuly"| AB["SchulyBackend"] + AB -->|"keeps a copy of your data"| ADB[("Schuly's database")] end - subgraph SOURCES["School systems (operator-provided)"] + subgraph private["Private mode"] direction TB - TokenProvider["Token-strategy provider"] - ScrapeProvider["Scrape-strategy provider"] + PU(["You"]) -->|"sign in to your school"| PP[("Your phone
holds the login")] + PP -->|"sent with each request"| PB["SchulyBackend
passes it straight through"] end - Sync -->|"proxy"| TokenProvider - Sync -->|"scrape"| ScrapeProvider - Stateless -->|"live, nothing stored"| TokenProvider - Stateless -->|"live, nothing stored"| ScrapeProvider + School[("Your school's system")] + AB -->|"syncs in the background"| School + PB -->|"reads live, stores nothing"| School ``` +## What private mode does on the device + +- The school login is written to the **device keystore** and never leaves it, apart from + being sent to the school system through the backend's anonymous proxy endpoints. +- The connect screen is generic: it renders whatever login fields the catalog lists for + your school, and follows the `privateAuthStrategy` it declares - `token` (a headless + login mints a bearer token and a refreshable session) or `scrape` (the credentials are + replayed on each fetch). +- If your school uses a one-time code, its seed is vaulted with the rest, and the + **Authenticator** screen generates codes on the device. +- When a session expires, the app re-connects silently from the keystore, so you are not + asked to sign in again. + | | Account mode | Private / secure mode | | ------------------- | ------------------------------ | ------------------------------------------------- | | Auth to Schuly | OIDC (Keycloak) bearer | **none** |