Fix · Users can see other users' data: fixing RLS and access rules

Users seeing another user's data after login (cached response)

After one person signs out and another signs in, the app shows the first account's profile, orders or settings instead of the new user's. The database rules are usually fine. A cache is storing per-user responses under a key that leaves out the user, in the client data layer, in browser storage or in a shared HTTP cache.

Likely causes, most common first

Cause 1 · in 1 of 1 matching cases

Client query cache keyed without the user ID

Data libraries such as TanStack Query store results under a query key. A key like ['profile'] is identical for every account, so after an account switch the app can reuse the previous user's result instead of fetching for the new one. TanStack's docs say a key should include every variable the query function depends on.

How to tell: The wrong data appears in the same browser after switching accounts, while a fresh private window signed in as the second user shows the correct rows.

Cause 2

Per-user data left in browser storage after sign-out

Apps persist state to localStorage or sessionStorage, and the browser's own HTTP cache can store and reuse responses for whoever is using it. If sign-out does not clear these, the next account in that browser starts from the previous account's data. Supabase's sign-out example clears local and session storage when the SIGNED_OUT event fires.

How to tell: After sign-out, the DevTools Application panel still lists the previous user's records in local or session storage.

Cause 3

Per-user responses stored in a shared cache

A CDN or proxy stores one response and reuses it for many users. A per-user response sent without private, or with public or s-maxage, can be stored there and served to other accounts. MDN warns that this can leak personal information.

How to tell: A separate device or browser that has not signed in as that user receives their data, and the response's Cache-Control header lacks private and no-store.

Check and fix it, step by step

  1. Check Cache-Control on per-user API responses

    Request an endpoint that returns the signed-in user's data and read its Cache-Control header. Per-user responses, especially those received after login or tied to a cookie session, should carry private or no-store, not public or s-maxage.

    curl -s -o /dev/null -D - -H "Authorization: Bearer $TOKEN" https://YOUR_APP/api/me | grep -i cache-control

    Docs: developer.mozilla.org →

  2. Find query keys that leave out the user

    Search for query keys on per-user data that hold a fixed string and nothing else. If the query function reads the current user, the key needs the user's ID as well.

    grep -rnE "queryKey: \[['\"][a-zA-Z]+['\"]\]" src

    Docs: tanstack.com →

  3. Stop caches storing per-user responses

    private keeps a response out of shared caches but lets the browser store it. no-store tells every cache, private or shared, not to store the response, which also covers account switches in the same browser.

    Cache-Control: no-store

    Docs: developer.mozilla.org →

  4. Put the user ID in every per-user query key

    Add the user ID to the key so each account gets its own cache entry and a new user triggers a new fetch.

    const { data } = useQuery({
      queryKey: ['profile', userId],
      queryFn: () => fetchProfile(userId),
    })

    Docs: tanstack.com →

  5. Clear browser storage when SIGNED_OUT fires

    Listen for the SIGNED_OUT event in onAuthStateChange and remove what the app stored for the previous user. Supabase documents the callback as safe without an async function.

    supabase.auth.onAuthStateChange((event) => {
      if (event === 'SIGNED_OUT') {
        [window.localStorage, window.sessionStorage].forEach((storage) => {
          Object.entries(storage).forEach(([key]) => storage.removeItem(key))
        })
      }
    })

    Docs: supabase.com →

  6. Send Clear-Site-Data on the sign-out response

    Add the header to the response that confirms sign-out so the browser drops its HTTP cache and DOM storage for your origin. Each directive needs double quotes.

    Clear-Site-Data: "cache", "storage"

    Docs: developer.mozilla.org →

Quick check: grep -rnE "queryKey: \[['\"][a-zA-Z]+['\"]\]" src # per-user keys with no user ID

How often this shows up in our data

1 of the 343 verified cases from the last 12 months in our data match this symptom (0.3%). The most common cause was “Client query cache keyed without the user ID” (1 of 1). How we collect and verify cases.

With Gemmein

On Gemmein, a private collection gives each signed-in user only their own records, so a fresh .list() after sign-in returns the new account's data. g.auth.currentUser() gives you the userId to put in every client cache key, and g.auth.logout() revokes the server session and clears the stored token.

Questions

Is Cache-Control: private enough to stop this?

private keeps the response out of shared caches such as a CDN, but the browser's own cache can still store and reuse it. If accounts switch in the same browser, use no-store on per-user endpoints or clear the cache on sign-out.


Does no-cache stop the response being stored?

No. MDN says no-cache lets a response be stored, but it must be revalidated before reuse. Use no-store to keep it out of caches.


Can a CDN cache a response to a request that sends an Authorization header?

By default, shared caches must not store those responses. Adding public, s-maxage or must-revalidate lifts that restriction, so check for those directives on per-user endpoints.


Why is Clear-Site-Data not doing anything?

Each directive must be wrapped in double quotes. A directive without them is invalid.


Sources

Every cause and step above was checked against these pages on 1 Oct 2026.

The broader pattern

This is one symptom of a wider failure pattern: Users can see other users' data: fixing RLS and access rules. The guide covers every cause we see for it, on any stack.

Get a heads-up when your stack breaks something

Leave your email and we'll let you know when something big changes for your stack. Unsubscribe any time by replying. Gemmein Limited. Research terms · Privacy

← All fixes