Appearance
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-storesafety 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 duplicatefetch()calls; it does not makeprivate, no-storeresponses 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:
- Backend
private, no-storeis intentional for auth/financial/bearer routes. SeeBloxclips-backend/docs/campaign-sponsor-reports.md:16(“URLs are credentials; forwarding a link forwards access”,no-store, no-referrer, noindex, 30 req/min/IP). - Frontend
cache: "no-store"is a defensive default, applied inconsistently. Proof:BloxClips-frontend/features/overview/api/overview.ts:12-53—fetchRecentSubmissions()usesno-storewhilefetchOverviewStats(),fetchActiveCampaigns(),fetchLeaderboard(),fetchOverviewTrends()on the same surface do not.surfaces/content-rewards/lib/adminFetch.ts:57sets nocachedefault; each call site adds it by hand. - 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, 10sTTLCache), frontendBloxClips-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). - No
@tanstack/react-queryinBloxClips-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/)
| Route | File:line | Reason |
|---|---|---|
GET /api/reports/:token, GET/POST/DELETE /api/admin/campaigns/:id/share-report | campaignReports.ts:8, adminCampaignReports.ts:10 | Bearer URL is the credential; contains creator gross payouts. Tests: campaignReports.test.ts:155,236 |
GET /api/campaign-invoices/:token | campaignInvoicePublic.ts:13 | Bearer invoice, financial. Test: campaignInvoicePublic.test.ts:106 |
Payout/financial contracts, GET /api/payouts/me, year-end/TIN export | payoutFinancialContracts.ts:123, payouts.ts:11, adminYearEnd.ts:74-75 | Balances, clawbacks, idempotency keys, TINs. Stale = wrong money. Export also sets Pragma: no-cache, Expires: 0 |
| Stats/earnings, admin analytics | stats.ts:90,116, adminAnalytics.ts:61 | CPM 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 previews | adminAuditEvents.ts:13, adminReviewHistory.ts:30, adminReviewAnalytics.ts:36, adminStaffTeam.ts:46, adminClipperGroups.ts:29, adminPvTracker.ts:20, supportChat.ts:129,139 | RBAC-gated ops/moderation content. Tests: adminAuditEvents.test.ts:11, adminReviewHistory.test.ts:12, adminReviewAnalytics.test.ts:12 |
GET /api/live/events (SSE) | live.ts:149 | no-cache, no-transform required or the stream breaks |
GET /api/environment-profile | environmentProfile.ts:7 | Diagnostics; 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/*viaadminFetch(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-storeon 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
| Surface | Files | Policy |
|---|---|---|
| Clipper count | surfaces/marketing/components/LiveCreatorNetworkStat.tsx:33, BrandStats.tsx:77, app/api/live/clipper-count/route.ts:83 | One shared ["clipper-count"], staleTime ~5s, refetchInterval 10s, refetchOnWindowFocus:false. Server stays public, max-age=5, SWR=20. Fixes 2× duplicate polls |
| Games/services strip | surfaces/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 proxies | app/api/roblox/thumbnail/[placeId]/route.ts:37, app/api/roblox/game/[placeId]/route.ts:144 | Keep 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>infinancialContracts.ts:26), coordinated invalidation instead ofbloxclips:notifications-changedfan-out. Hand cache inuseAdminAlerts.ts:39-75is the migration template. adminFetch.ts:57429-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.mdwith §0, §2-frontend, §3, §4, §5 condensed + link here. Append pointer after<!-- END:nextjs-agent-rules -->inBloxClips-frontend/AGENTS.md(preserve that block;next devregenerates it). Baseorigin/dev, PR basedev. Blocked 2026-09-16 by active leasecodex-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.mdwith §0, §2-backend, §4-budget rule condensed + link here. Link fromdocs/campaign-sponsor-reports.md,docs/rbac/authorization.mdif present. Baseorigin/dev, PR basedev. Blocked 2026-09-16 by dirty leasecodex-bloxclips:backend(.../Bloxclips-backend-326cdd/2/Bloxclips-backend, branchfix/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-82no-cacheto TikTok) and local observability UI (src/observability/server.ts:11no-store,src/observability/ui.ts:352scache:'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:36assertsno-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,BrandStatssame), 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.