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

nickdetsy26-creator·5 days ago