Document expired-token causes, client limits, and server-side options (TTL, scoped tokens, refresh, BFF) plus questions for Endorser maintainers.
6.8 KiB
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:
{
"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
- Align on security posture: Is a longer TTL or scoped long-lived read token acceptable for Endorser?
- If not, is refresh token in native (option 3) or BFF (option 4) on the roadmap?
- 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)
- What is the current access token TTL and can it be increased for mobile clients, or per-scope?
- Is refresh token (or similar) available for non-interactive renewal?
- Would a read-only scope for
plansLastUpdatedBetweenwith a longer lifetime be acceptable? - 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.