Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 26 additions & 45 deletions docs/architecture-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>(Keycloak)"]
ApiClient["ApiClient<br/>Bearer token + auto-refresh"]
Backend[("SchulyBackend<br/>authenticated /api/*")]
DB[("PostgreSQL<br/>data stored per user")]
Sync["Plugin sync tasks<br/>(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<br/>clean Dio (no auth interceptor)"]
AnonCat[("GET /api/app/school-systems<br/>[AllowAnonymous]")]
Connect["Generic connect screen<br/>renders loginFields, branches on privateAuthStrategy"]
TP["TokenProxyClient<br/>clean Dio"]
SP["ScrapeProxyClient<br/>clean Dio"]
Stateless[("Backend stateless proxy<br/>/api/plugins/*/stateless/*<br/>[AllowAnonymous] - stores nothing")]
Keystore[("On-device keystore only")]
Auth["Authenticator screen<br/>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<br/>holds the login")]
PP -->|"sent with each request"| PB["SchulyBackend<br/>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** |
Expand Down
Loading