App Router list view stays stale

Hi everyone! I’m debugging some stubborn cache/stale-data behaviour in an App Router project and would love a second pair of eyes.

Environment
Next.js 15.4.0-canary.109 (App Router, running with next dev --turbopack)
Node.js 22.17.0
Experimental partial prerendering enabled with ppr: ‘incremental’
Data comes from a Directus REST API; we’re not using the Next.js app/api routes for this entity.
What we expect
After creating/updating/deleting a customer we call revalidateTag(‘customersSICC’) and revalidatePath(‘/dashboard/customersSICC’), then redirect back to the list. The list view should show the fresh data immediately.
What actually happens
Go to /dashboard/customersSICC (this page uses export const dynamic = ‘force-dynamic’ and renders a wrapped in ).
Click “Crear Cliente”, fill out the form, submit.
The server action runs, writes to Directus, calls revalidateTag/revalidatePath, and redirects back.
The list view shows the old data until we manually refresh the browser. The same thing happens after edit/delete. Client-side navigation via our RefreshLink (which calls router.refresh() inside startTransition) doesn’t help.

// app/lib/actions.ts
export async function createCustomerSICC(formData: FormData) {
  const name = String(formData.get('name') || '');
  const body = {
    status: formData.get('status'),
    name,
    CUIT: formData.get('CUIT'),
    contacto: formData.get('contacto'),
    mail: formData.get('mail'),
    tel: formData.get('tel'),
    mailNotif: formData.get('mailNotif'),
    urlSlug: slugify(name),
  };

  const response = await fetch(`${DIRECTUS_URL}/items/Clientes`, {
    next: { revalidate: 0 },
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });

  await response.json(); // Directus returns the created record

  revalidateTag('customersSICC');
  revalidatePath('/dashboard/customersSICC');
  revalidatePath('/', 'layout');
  redirect('/dashboard/customersSICC');
}

export async function deleteCustomerSICC(id: string) {
  await fetch(`${DIRECTUS_URL}/items/Clientes/${id}`, {
    next: { revalidate: 0 },
    method: 'DELETE',
  });
  revalidateTag('customersSICC');
  revalidatePath('/dashboard/customersSICC');
  redirect('/dashboard/customersSICC');
}

// app/lib/data.ts
const ITEMS_PER_PAGE = 6;

export async function getCustomersSICC(query = "", currentPage = 1) {
  const url = `${DIRECTUS_URL}/items/Clientes?page=${currentPage}&limit=${ITEMS_PER_PAGE}&sort=name${
    query ? `&filter[name][_contains]=${encodeURIComponent(query)}` : ''
  }`;

  try {
    const res = await fetch(url, {
      next: { tags: ['customersSICC'] },
    });
    if (!res.ok) throw new Error('Failed to fetch customers');
    const data = await res.json();
    return data.data;
  } catch (err) {
    console.error('Fetch customers failed', err);
    return [];
  }
}

The list page simply calls getCustomersSICC(query, currentPage) inside the server component and renders the result. Pagination counts come from a similar Directus request using the same tag (that request is cache: 'no-store').

Things we’ve tried

  • Ensured the page is dynamic (export const dynamic = 'force-dynamic').

  • Added revalidatePath('/', 'layout') for good measure.

  • Confirmed redirect('/dashboard/customersSICC') happens after the revalidation calls.

  • Wrapped our navigation buttons with a custom RefreshLink that does router.push(...) + router.refresh() inside startTransition.

  • Verified that Directus receives the mutation and returns the new record ID.

Still, the data in the table is stale until a hard refresh. Are we missing something about how revalidateTag interacts with fetches that omit cache: 'no-store' in Next 15? Could partial prerendering be keeping the old snapshot around even with force-dynamic? Any guidance on diagnosing this would be hugely appreciated!