Skip to content

Status note (2026-09-25): Reviewed 2026-09-16. The no-store safety rules remain important; verify implementation details and line references against current frontend/backend source and tests before changing cache behavior.

Data-fetching and cache policy (source of truth) ​

TL;DR: rules for how frontend pages fetch data and when caching is forbidden. Read this before adding or changing any data-fetching code. The no-store safety rules still apply; file/line references may be stale — verify against current source and tests.

Status: investigation complete 2026-09-16. TanStack Query pilot scoped separately (public marketing reads only). This doc records why no-store exists, where it must stay, and what new pages/features must do.

Read with: docs/agent-orchestration/PROJECT.md, docs/agent-orchestration/GITHUB.md, root AGENTS.md (Treehouse isolation + financial invariants).

0. One-paragraph rule ​

  • Backend Cache-Control: private, no-store = do not store this response anywhere (browser, CDN, proxy). Required for authenticated, financial, bearer-URL, and moderation data.
  • Frontend fetch(..., { cache: "no-store" }) = bypass Next.js/browser HTTP cache for this request. Required when the response is user-specific or money-sensitive.
  • TanStack Query = in-memory client dedup + staleTime, not an HTTP-cache override. It reduces duplicate fetch() calls; it does not make private, no-store responses shareable. Never use a persister for financial keys. Never share a query key across users.

1. Verdict from audit ​

Blanket no-store is half-intentional, half-copy-paste:

  1. Backend private, no-store is intentional for auth/financial/bearer routes. See Bloxclips-backend/docs/campaign-sponsor-reports.md:16 (“URLs are credentials; forwarding a link forwards access”, no-store, no-referrer, noindex, 30 req/min/IP).
  2. Frontend cache: "no-store" is a defensive default, applied inconsistently. Proof: BloxClips-frontend/features/overview/api/overview.ts:12-53 — fetchRecentSubmissions() uses no-store while fetchOverviewStats(), fetchActiveCampaigns(), fetchLeaderboard(), fetchOverviewTrends() on the same surface do not. surfaces/content-rewards/lib/adminFetch.ts:57 sets no cache default; each call site adds it by hand.
  3. Public cacheable exceptions already exist and are correct: backend Bloxclips-backend/src/api/routes/live.ts:90-104 (public, max-age=5, stale-while-revalidate=20, 10s TTLCache), frontend BloxClips-frontend/app/api/live/clipper-count/route.ts:83,90, app/api/roblox/thumbnail/[placeId]/route.ts:37 (s-maxage=3600), app/api/roblox/game/[placeId]/route.ts:144 (s-maxage=300).
  4. No @tanstack/react-query in BloxClips-frontend/package.json (Next 16.3.3, React 19). git log -S "no-store" shows headers added per-feature (backend #85, #93, #95, #98, #107, #111), never a migration away from a cache library.

2. Must stay no-store (do not cache, do not add staleTime > 0) ​

Backend (Bloxclips-backend/src/api/routes/) ​

RouteFile:lineReason
GET /api/reports/:token, GET/POST/DELETE /api/admin/campaigns/:id/share-reportcampaignReports.ts:8, adminCampaignReports.ts:10Bearer URL is the credential; contains creator gross payouts. Tests: campaignReports.test.ts:155,236
GET /api/campaign-invoices/:tokencampaignInvoicePublic.ts:13Bearer invoice, financial. Test: campaignInvoicePublic.test.ts:106
Payout/financial contracts, GET /api/payouts/me, year-end/TIN exportpayoutFinancialContracts.ts:123, payouts.ts:11, adminYearEnd.ts:74-75Balances, clawbacks, idempotency keys, TINs. Stale = wrong money. Export also sets Pragma: no-cache, Expires: 0
Stats/earnings, admin analyticsstats.ts:90,116, adminAnalytics.ts:61CPM depletes budget / RPM pays creators; stale shows pre-exhaustion eligibility. Test: adminAnalytics.test.ts:25
Audit, review history/analytics, staff-team, clipper-groups, PV-tracker, support-chat staff previewsadminAuditEvents.ts:13, adminReviewHistory.ts:30, adminReviewAnalytics.ts:36, adminStaffTeam.ts:46, adminClipperGroups.ts:29, adminPvTracker.ts:20, supportChat.ts:129,139RBAC-gated ops/moderation content. Tests: adminAuditEvents.test.ts:11, adminReviewHistory.test.ts:12, adminReviewAnalytics.test.ts:12
GET /api/live/events (SSE)live.ts:149no-cache, no-transform required or the stream breaks
GET /api/environment-profileenvironmentProfile.ts:7Diagnostics; public but sensitive config

Financial invariants that force freshness: CampaignInvoice -> Receipt -> Allocation -> exactly-once bridge; pending-at-exhaustion not paid; paused views not paid; top-up/resume establish new earning boundaries. Any cached budget/earnings/payout-status display violates these.

Frontend (BloxClips-frontend/) ​

  • features/payouts/financialContracts.ts:63 (financialFetch, credentials:include, cache:no-store).
  • All features/admin/* via adminFetch (credentials:include): adminStaff.ts:13,20, adminClipperGroups.ts:26, adminPvTracker.ts:19, adminAffiliates.ts:6, adminSubmissions.ts:64, adminUserList.ts:21, campaignShareReports.ts:11, adminAuditEvents.ts:24,31.
  • User-scoped: features/submissions/api/submissions.ts:26, features/overview/api/overview.ts:38, features/campaigns/components/CampaignLaunchPopup.tsx:84,97, surfaces/content-rewards/components/NotificationCenter.tsx:102 (120s poll + event), surfaces/content-rewards/components/LiveSessionUpdates.tsx:41 (/api/auth/me, 60s).
  • Bearer publics that stay no-store on fetch (time-sensitive links): features/reports/api/publicReport.ts:17, features/admin/campaign-management/api/publicCampaignInvoice.ts:8 (credentials:omit). TanStack may dedup the 60s poll (features/reports/hooks/useSponsorReport.ts:7,43) but must not extend freshness.

3. Safe to cache / TanStack pilot surface ​

SurfaceFilesPolicy
Clipper countsurfaces/marketing/components/LiveCreatorNetworkStat.tsx:33, BrandStats.tsx:77, app/api/live/clipper-count/route.ts:83One shared ["clipper-count"], staleTime ~5s, refetchInterval 10s, refetchOnWindowFocus:false. Server stays public, max-age=5, SWR=20. Fixes 2× duplicate polls
Games/services stripsurfaces/marketing/components/ActiveCampaignsStrip.tsx:184-186["games"] / ["services"], staleTime 5m, mount-only, no polling
Top videos (stretch)features/campaigns/api/topPerformingVideos.ts:17["top-videos", campaignId], staleTime 30s, 404→[] preserved
Roblox proxiesapp/api/roblox/thumbnail/[placeId]/route.ts:37, app/api/roblox/game/[placeId]/route.ts:144Keep existing s-maxage; no client change

Keep { cache: "no-store" } inside pilot queryFns for now (HTTP stays uncached; TanStack provides mem dedup). Do not switch to next: { revalidate } in the pilot.

4. Decision tree for new pages/features (required checklist) ​

1. Auth, financial, bearer-URL, user-specific, or moderation data?
   YES → private,no-store + credentials:include + no TanStack persister.
         Query key MUST include userId and/or token. staleTime = 0.
         Invalidate on mutation; never share across users.
   NO  → public marketing / reference data?
         YES → shared queryKey + staleTime/gcTime + refetchInterval only if live.
               Prefer server public max-age + SWR (see live.ts:93).
         NO  → ambiguous → default to no-store and record why in the PR.
2. Does this display budget, earnings, payout status, or eligibility?
   YES → staleTime = 0, refetch on focus/resume/top-up/funding event.
3. Copy-paste check: did I add { cache:"no-store" } because neighbors have it?
   If yes, justify per-row (see §2 vs §3) or remove.

Additional bans: no staleTime > 0 for budget/payout/eligibility; no cross-user keys; no persister for financial keys; no Cache-Control relaxation without backend review + test asserting the header; no logging of /share/report/* or /api/reports/* URLs (they are credentials).

5. TanStack rules (post-pilot) ​

  • Provider at narrowest shared parent, not app root if it forces admin routes to load it. Defaults: staleTime 5s, gcTime 30s, retry 1, refetchOnWindowFocus false, refetchOnReconnect true.
  • Keys: ["clipper-count"], ["games"], ["services"], ["top-videos", campaignId], ["report", token], ["admin-alerts", userId], ["notifications", userId]. Never omit the user/token segment.
  • Admin tables: use keepPreviousData/placeholderData, cursor pagination (Page<T> in financialContracts.ts:26), coordinated invalidation instead of bloxclips:notifications-changed fan-out. Hand cache in useAdminAlerts.ts:39-75 is the migration template.
  • adminFetch.ts:57 429-retry stays for admin calls; do not wrap public pilot queries with it.

6. Per-repo application ​

  • Workspace (this file): source of truth. Linked from docs/README.md.
  • Frontend: create BloxClips-frontend/docs/data-fetching.md with §0, §2-frontend, §3, §4, §5 condensed + link here. Append pointer after <!-- END:nextjs-agent-rules --> in BloxClips-frontend/AGENTS.md (preserve that block; next dev regenerates it). Base origin/dev, PR base dev. Blocked 2026-09-16 by active lease codex-bloxclips:frontend (.../BloxClips-frontend-207dad/3/BloxClips-frontend) — apply via fresh lease when free; staged payload in /tmp/opencode/bloxclips-docs/frontend-data-fetching.md.
  • Backend: create Bloxclips-backend/docs/cache-policy.md with §0, §2-backend, §4-budget rule condensed + link here. Link from docs/campaign-sponsor-reports.md, docs/rbac/authorization.md if present. Base origin/dev, PR base dev. Blocked 2026-09-16 by dirty lease codex-bloxclips:backend (.../Bloxclips-backend-326cdd/2/Bloxclips-backend, branch fix/retire-legacy-whop-env, 20+ modified files) — do not interfere; staged payload in /tmp/opencode/bloxclips-docs/backend-cache-policy.md.
  • metric-scraper: out of scope. Only hits are outbound crawler headers (src/crawlers/tiktok.ts:81-82 no-cache to TikTok) and local observability UI (src/observability/server.ts:11 no-store, src/observability/ui.ts:35 2s cache:'no-store' poll). No app cache-policy doc needed.

7. Evidence pointers ​

  • Frontend inventory: 21 cache:"no-store" sites (§2–§3); tests/public-report-api.test.mjs:36 asserts no-store.
  • Backend inventory: ~17 route files (§2); header tests in adminAuditEvents.test.ts:11, adminAnalytics.test.ts:25, adminReviewHistory.test.ts:12, adminReviewAnalytics.test.ts:12, campaignReports.test.ts:155,163,236, campaignInvoicePublic.test.ts:106.
  • Polling: sponsor report 60s (useSponsorReport.ts:7,43), notifications 120s (NotificationCenter.tsx:187), session 60s (LiveSessionUpdates.tsx:67, AdminAccessContext.tsx:54), admin alerts 30s (useAdminAlerts.ts:9,111), clipper-count 10s (LiveCreatorNetworkStat.tsx:5, BrandStats same), EmailGate 4s (EmailGate.tsx:58), backend live poll 15s (live.ts:13).
  • History: git log -S "no-store" per-feature; no TanStack/SWR adoption in history.