Bug Reports
Open

Admin permissions toggles silently revert after navigation (TanStack Query cache stale after save)

just an FYI

----

Summary

Toggling any permission on /admin/settings/permissions (Anonymous voting, Public view, Submissions, etc.) appears to save successfully — no error in UI or logs — but the new value doesn't reflect when navigating away and back to the page. A hard browser refresh (Cmd+R) shows the correct saved value, confirming the DB write succeeded; the UI is reading stale data.

Reproduce

  1. Go to /admin/settings/permissions as admin
  2. Toggle "Anonymous voting" OFF
  3. Navigate to another admin page (e.g. /admin/settings)
  4. Navigate back to /admin/settings/permissions
  5. Toggle appears ON again
  6. Hard browser refresh — toggle now correctly shows OFF

Verification: DB write succeeds

SELECT (portal_config::jsonb -> 'features' ->> 'anonymousVoting') FROM settings;

-- Returns 'false' after toggle, as expected

So the persistence layer is fine. The bug is purely client-side cache.

Root cause

In apps/web/src/routes/admin/settings.permissions.tsx, updateFeature():

await updatePortalConfigFn({ data: { features: { [key]: value } } })

startTransition(() => {

router.invalidate()

})

router.invalidate() re-runs the route loader, but the loader uses queryClient.ensureQueryData(settingsQueries.portalConfig()). With staleTime: STALE_TIME_LONG on the query, ensureQueryData returns the cached value — the stale pre-save one — instead of refetching.

useState(features?.anonymousVoting ?? true) then initializes from the stale cache → toggle reads as ON.

Suggested fix

Invalidate the specific TanStack Query after the save:

await updatePortalConfigFn({ data: { features: { [key]: value } } })

await queryClient.invalidateQueries({ queryKey: ['settings', 'portalConfig'] })

startTransition(() => {

router.invalidate()

})

Requires useQueryClient import + const queryClient = useQueryClient() in the component. Verified locally — fully resolves the issue.

Scope (probably affects other pages too)

The same await mutation + router.invalidate() pattern without explicit queryClient.invalidateQueries likely exists on other admin pages that use useSuspenseQuery against a long-staleTime query. Worth auditing:

  • admin/settings/portal (welcome card)
  • admin/settings/branding
  • admin/settings/widget
  • admin/settings/help-center

Environment

  • Quackback v0.10.5 (self-hosted Docker)
  • Reproduces deterministically — not a race condition

0 Comments

Sign in to comment

No comments yet. Be the first to share your thoughts!