# ADR-004: Favorites use Gitea star via a new `write:user` scope ## Status Accepted — 2026-08-26 ## Context The human product owner asked for a Favorites tab. Two designs were considered: a device-local favorite flag (no server write, no new scope) or Gitea's native star feature (`GET/PUT/DELETE /user/starred/{owner}/{repo}`), which syncs with the Gitea web UI and across devices but requires a write call. ADR-002 explicitly required product approval, an ADR revision, and explicit reauthorization UX before any new scope was added — this ADR is that revision. The human product owner chose Gitea-native star after being told explicitly that it requires the app's first write endpoint, that no NAS/Gitea server restart or configuration is needed (starring is a standard, already-live Gitea REST endpoint), and that the only consequence is a fresh login to pick up the new scope. ## Decision - Add `write:user` to the app's requested OAuth scopes. Per Gitea's OAuth2 provider documentation, `write:user` grants read/write access to user-scoped operations including repository subscriptions, which covers starring. - `OAuthClientConfig`'s scope guard is relaxed from "every scope must be read-only" to "every scope must be read-only or appear in an explicit, hard-coded `EXPLICITLY_APPROVED_WRITE_SCOPES` allow-list." Today that list contains exactly `write:user`. Adding any other write scope later requires the same kind of explicit human decision and ADR, not just adding an enum value. - Star/unstar are the only write calls added. No create/edit/merge/close/comment/delete/admin endpoint is introduced by this or any related task (TASK-018). - Star/unstar require no confirmation dialog: both are immediately reversible (tap again to undo) and non-destructive (no data loss, no effect on other users), matching Gitea's and GitHub's own web UI convention and `AGENTS.md`'s confirmation requirement, which is scoped to destructive operations. ## Alternatives considered - Device-local-only favorites: rejected by the human product owner in favor of cross-device/web parity, despite being the option with zero new scope and zero server interaction. - A generic "allow any write scope" guard relaxation: rejected — kept the allow-list explicit and enumerable so a future write scope cannot be added without deliberately touching this decision again. ## Consequences - Existing signed-in users must log in again once before they can use Favorites; their existing token only carries the old read-only scopes. - If this Gitea instance's OAuth application registration restricts requestable scopes independently of what the client requests (a server-admin setting, not something requiring a restart), the first login attempt with the new scope will surface as an explicit authorization error rather than silently degrading; this is a known, accepted risk to verify on-device rather than something assumed away. - Favorite state is not cached locally; the starred set is always read fresh from Gitea, consistent with the product's existing no-offline-cache posture. ## Evidence - Gitea REST API: `PUT/DELETE /user/starred/{owner}/{repo}`, `GET /user/starred` — standard, already-live endpoints (https://docs.gitea.com/api/1.27.2/). - Gitea OAuth2 provider documentation describes `write:user` as granting "read/write/delete access to user operations, such as updating user repo subscriptions" (https://docs.gitea.com/development/oauth2-provider/).