3.6 KiB
ADR-004: Favorites use Gitea star via a new write:user scope
Status: suspended by TASK-019 after target-server authorization failure. Production defaults have returned to read-only scopes; this decision must not be used as release authorization.
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:userto the app's requested OAuth scopes. Per Gitea's OAuth2 provider documentation,write:usergrants 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-codedEXPLICITLY_APPROVED_WRITE_SCOPESallow-list." Today that list contains exactlywrite: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:useras granting "read/write/delete access to user operations, such as updating user repo subscriptions" (https://docs.gitea.com/development/oauth2-provider/).