# Options: expired JWT during background “New Activity” prefetch (mobile) **Date:** 2026-03-26 17:29 PST **Audience:** TimeSafari / crowd-funder team; **Endorser server** maintainers (auth + API policy) **Context:** Android Capacitor app, `POST /api/v2/report/plansLastUpdatedBetween`, native `TimeSafariNativeFetcher` invoked from WorkManager at **T−5 minutes** before the daily notification. --- ## Problem (short) New Activity notifications prefetch Endorser data in **background** (no JavaScript, no WebView). The HTTP client uses a **Bearer JWT** supplied earlier via `configureNativeFetcher` / `getHeaders(activeDid)`. If the **access token’s `exp`** is **before** prefetch time, the API returns **400** with a body like: ```json { "error": { "message": "JWT failed verification: ... JWT has expired: exp: … < now: …", "code": "JWT_VERIFY_FAILED" } } ``` We **cannot** rely on the user opening the app immediately before prefetch (T−5), so **client-only** mitigations (e.g. refresh JWT on app resume) **reduce** failures but **do not guarantee** a valid token for headless background work. --- ## Why this is different from normal in-app API calls | In-app | Background prefetch | |--------|----------------------| | `getHeaders()` runs in JS when needed; user often recently active | WorkManager runs **without** Capacitor / passkey / session refresh | | Short TTL tokens are refreshed as the user uses the app | Same token may sit in native memory until **T−5** (or longer) | So **server-side** and **architecture** choices matter for this feature. --- ## Options (for decision) ### 1. Increase access token TTL (Endorser / IdP) **Idea:** Issue access JWTs with a longer `exp` so that **configure time → prefetch time** (often **5+ minutes**, sometimes **24h+** if the user rarely opens the app) usually still falls inside validity. | Pros | Cons | |------|------| | Simple to explain; one policy change | Longer-lived bearer tokens increase risk if exfiltrated; mitigate with scope, rotation, monitoring | | No client protocol change | May not fit strict security posture without a dedicated scope | **Endorser owner:** token lifetime, scopes, and whether a **dedicated** lifetime or scope for “mobile background read” is acceptable. --- ### 2. Scoped long-lived token for report reads only (Endorser) **Idea:** Mint a **separate** access token (or sub-scope) valid only for **read-only report** endpoints (`plansLastUpdatedBetween`, etc.), with a **longer TTL** than the interactive session token. | Pros | Cons | |------|------| | Limits blast radius vs “longer JWT for everything” | Requires auth model + issuance path; client must store/use this token only for prefetch | **Endorser owner:** feasibility of **narrow scope** + **longer TTL** for this use case. --- ### 3. Refresh token or device grant (Endorser + mobile native) **Idea:** Client stores a **refresh token** (or OAuth **device** grant) in **Android Keystore / iOS Keychain**. Before `plansLastUpdatedBetween`, **native** code (no JS) exchanges it for a **new access token**. | Pros | Cons | |------|------| | Standard pattern; short TTL for access tokens remains | Endorser must support refresh (or equivalent); secure storage + rotation; **both** client and server work | | Works when app is backgrounded for days | Implementation cost on mobile | **Endorser owner:** refresh endpoint, token rotation, revocation. **Mobile owner:** native fetch path, secure storage, failure handling. --- ### 4. Backend proxy / BFF (TimeSafari backend + Endorser) **Idea:** Phone calls **your** backend with a **device session** (or FCM registration id); **server** uses **server-to-server** credentials or a **service account** to call Endorser. The device **never** sends an Endorser JWT for this path. | Pros | Cons | |------|------| | No Endorser JWT lifetime problem on device | New service, auth, rate limits, privacy review | | Central place for logging, abuse control | Operational cost | **Endorser owner:** partner / S2S auth model for the BFF. **Product team:** hosting and trust boundaries. --- ### 5. “Cron” or periodic jobs on the device to refresh JWT (JS) **Idea:** Use something like a **cron** schedule to refresh tokens. **Reality:** Scheduled **native** jobs can run, but **Capacitor / `getHeaders()` / passkey** do **not** run reliably in that context without waking the **WebView**. So **“cron”** only helps if refresh is **fully native** (see option 3) or you accept **unreliable** wake + JS. **Not recommended** as the primary fix unless paired with **native refresh** or **server** changes. --- ### 6. Product / UX constraints (no server change) **Idea:** Accept that **headless** API calls may fail if the session is stale; show **fallback** copy; or require “open app once per day” for best results. | Pros | Cons | |------|------| | No Endorser change | Does not meet “API-driven notification” expectation for inactive users | --- ## Client-side mitigations already in play (not sufficient alone) - **`configureNativeFetcherIfReady()`** after startup and when **Account** / identity is ready. - **`appStateChange` → `isActive`:** refresh native fetcher when the app returns to foreground (reduces staleness when the user **does** open the app). - **Error logging** of 400 bodies for diagnosis. These **do not** guarantee a fresh JWT at **T−5** if the user never opens the app before prefetch. --- ## Suggested decision order 1. **Align on security posture:** Is a **longer TTL** or **scoped long-lived read token** acceptable for Endorser? 2. If not, is **refresh token in native** (option 3) or **BFF** (option 4) on the roadmap? 3. **Parallel:** UX fallback when API is unavailable (option 6) so the app never silently looks “broken.” --- ## References (this repo) | Topic | Location | |--------|----------| | Native fetcher + JWT from `getHeaders` | `src/services/notifications/nativeFetcherConfig.ts` | | Android POST + errors | `android/app/src/main/java/app/timesafari/TimeSafariNativeFetcher.java` | | Web `plansLastUpdatedBetween` + `afterId` | `src/libs/endorserServer.ts` (`getStarredProjectsWithChanges`) | | New Activity / dual schedule | `doc/notification-from-api-call.md`, `doc/plugin-feedback-android-dual-schedule-native-fetch-and-timing.md` | --- ## Open questions for Endorser (server developer) 1. What is the **current access token TTL** and can it be **increased** for mobile clients, or **per-scope**? 2. Is **refresh token** (or similar) available for **non-interactive** renewal? 3. Would a **read-only** scope for `plansLastUpdatedBetween` with a **longer** lifetime be acceptable? 4. Is there an existing **server-to-server** or **partner** path that a **BFF** could use instead of user JWT on device? --- *This document is for internal planning and decision; update it when the team chooses an approach.*