Files
crowd-funder-for-time-pwa/doc/endorser-jwt-background-prefetch-options.md
Jose Olarte III 43c9b95c14 docs: add Endorser JWT options for background New Activity prefetch
Document expired-token causes, client limits, and server-side options (TTL,
scoped tokens, refresh, BFF) plus questions for Endorser maintainers.
2026-03-26 18:16:54 +08:00

6.8 KiB
Raw Blame History

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 T5 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 tokens 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 (T5), 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 T5 (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.
  • appStateChangeisActive: 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 T5 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.