diff --git a/apps/sim/app/workspace/[workspaceId]/files/page.tsx b/apps/sim/app/workspace/[workspaceId]/files/page.tsx
index ab21f2f3b72..2ed876e4ba0 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/page.tsx
@@ -1,5 +1,8 @@
import { Suspense } from 'react'
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
import { Files } from './files'
import FilesLoading from './loading'
@@ -15,10 +18,17 @@ export const metadata: Metadata = {
* table headers) so a suspend never shows a blank frame; the route-level
* `loading.tsx` covers the navigation/chunk-load transition the same way.
*/
-export default function FilesPage() {
+export default async function FilesPage({ params }: { params: Promise<{ workspaceId: string }> }) {
+ const { workspaceId } = await params
+
+ const queryClient = getQueryClient()
+ await prefetchFilesBrowser(queryClient, workspaceId)
+
return (
- }>
-
-
+
+ }>
+
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/files/prefetch.ts
new file mode 100644
index 00000000000..8780aa537fa
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/files/prefetch.ts
@@ -0,0 +1,43 @@
+import type { QueryClient } from '@tanstack/react-query'
+import type { WorkspaceFileFolderApi } from '@/lib/api/contracts/workspace-file-folders'
+import type { ListWorkspaceFilesResponse } from '@/lib/api/contracts/workspace-files'
+import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
+import { workspaceFileFolderKeys } from '@/hooks/queries/workspace-file-folders'
+import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
+
+/**
+ * Prefetches the Files browser's two lists — workspace files and file folders —
+ * under the same query keys their client hooks (`useWorkspaceFiles`,
+ * `useWorkspaceFileFolders`) use (scope `active`), so the browser paints
+ * populated on first render.
+ *
+ * Both payloads carry `Date` fields, so they go through their routes and cache
+ * the serialized wire shape — see {@link prefetchInternalJson}.
+ */
+export async function prefetchFilesBrowser(
+ queryClient: QueryClient,
+ workspaceId: string
+): Promise {
+ await Promise.all([
+ queryClient.prefetchQuery({
+ queryKey: workspaceFilesKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const data = await prefetchInternalJson(
+ `/api/workspaces/${workspaceId}/files?scope=active`
+ )
+ return data.success ? data.files : []
+ },
+ staleTime: 30 * 1000,
+ }),
+ queryClient.prefetchQuery({
+ queryKey: workspaceFileFolderKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const data = await prefetchInternalJson<{ folders?: WorkspaceFileFolderApi[] }>(
+ `/api/workspaces/${workspaceId}/files/folders?scope=active`
+ )
+ return data.folders ?? []
+ },
+ staleTime: 30 * 1000,
+ }),
+ ])
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/page.tsx b/apps/sim/app/workspace/[workspaceId]/home/page.tsx
index e29acc640ee..13595d65398 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/page.tsx
@@ -1,6 +1,9 @@
import { Suspense } from 'react'
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
import { getSession } from '@/lib/auth'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import { prefetchHomeLists } from '@/app/workspace/[workspaceId]/home/prefetch'
import { Home } from './home'
import { HomeFallback } from './home-fallback'
@@ -8,11 +11,20 @@ export const metadata: Metadata = {
title: 'New chat',
}
-export default async function HomePage() {
+export default async function HomePage({ params }: { params: Promise<{ workspaceId: string }> }) {
+ const { workspaceId } = await params
+
+ const queryClient = getQueryClient()
+ const listsPrefetch = prefetchHomeLists(queryClient, workspaceId)
+
const session = await getSession()
+ await listsPrefetch
+
return (
- }>
-
-
+
+ }>
+
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/home/prefetch.ts
new file mode 100644
index 00000000000..956a9e95555
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/prefetch.ts
@@ -0,0 +1,48 @@
+import type { QueryClient } from '@tanstack/react-query'
+import type { FolderApi } from '@/lib/api/contracts'
+import type { ListWorkspaceFilesResponse } from '@/lib/api/contracts/workspace-files'
+import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
+import { FOLDER_LIST_STALE_TIME, mapFolder } from '@/hooks/queries/folders'
+import { folderKeys } from '@/hooks/queries/utils/folder-keys'
+import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
+
+/**
+ * Prefetches the home page's secondary lists — folders and workspace files —
+ * under the same query keys their client hooks (`useFolders`,
+ * `useWorkspaceFiles`) use, so the home view paints populated on first render.
+ *
+ * The workflow list (`workflowKeys.list(ws, 'active')`) is already hydrated by
+ * the workspace sidebar prefetch and is intentionally not repeated here.
+ *
+ * Folders are fetched through the route and mapped with the same `mapFolder`
+ * the hook applies, matching its cached shape (string dates → `Date`). Files
+ * carry `Date` fields, so they go through the route and cache the serialized
+ * wire shape — see {@link prefetchInternalJson}.
+ */
+export async function prefetchHomeLists(
+ queryClient: QueryClient,
+ workspaceId: string
+): Promise {
+ await Promise.all([
+ queryClient.prefetchQuery({
+ queryKey: folderKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const { folders } = await prefetchInternalJson<{ folders?: FolderApi[] }>(
+ `/api/folders?workspaceId=${workspaceId}&scope=active`
+ )
+ return (folders ?? []).map(mapFolder)
+ },
+ staleTime: FOLDER_LIST_STALE_TIME,
+ }),
+ queryClient.prefetchQuery({
+ queryKey: workspaceFilesKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const data = await prefetchInternalJson(
+ `/api/workspaces/${workspaceId}/files?scope=active`
+ )
+ return data.success ? data.files : []
+ },
+ staleTime: 30 * 1000,
+ }),
+ ])
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx
index be3743be659..6243dc42035 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx
@@ -1,8 +1,26 @@
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
+import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
import { Knowledge } from './knowledge'
export const metadata: Metadata = {
title: 'Knowledge Base',
}
-export default Knowledge
+export default async function KnowledgePage({
+ params,
+}: {
+ params: Promise<{ workspaceId: string }>
+}) {
+ const { workspaceId } = await params
+
+ const queryClient = getQueryClient()
+ await prefetchKnowledgeBases(queryClient, workspaceId)
+
+ return (
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts
new file mode 100644
index 00000000000..0e8e5578840
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/prefetch.ts
@@ -0,0 +1,28 @@
+import type { QueryClient } from '@tanstack/react-query'
+import type { KnowledgeBaseData } from '@/lib/api/contracts/knowledge'
+import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
+import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
+
+/**
+ * Prefetches the workspace's knowledge-bases list under the same query key the
+ * client `useKnowledgeBasesQuery` hook uses (scope `active`), so the list paints
+ * populated on first render.
+ *
+ * The list carries `Date` fields, so it goes through the `/api/knowledge` route
+ * and caches the serialized wire shape — see {@link prefetchInternalJson}.
+ */
+export async function prefetchKnowledgeBases(
+ queryClient: QueryClient,
+ workspaceId: string
+): Promise {
+ await queryClient.prefetchQuery({
+ queryKey: knowledgeKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const result = await prefetchInternalJson<{ data: KnowledgeBaseData[] }>(
+ `/api/knowledge?workspaceId=${workspaceId}&scope=active`
+ )
+ return result.data
+ },
+ staleTime: 60 * 1000,
+ })
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts b/apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts
new file mode 100644
index 00000000000..e48f6064c17
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/lib/prefetch-internal-fetch.ts
@@ -0,0 +1,25 @@
+import { headers } from 'next/headers'
+import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
+
+/**
+ * Server-side GET against an internal `/api` route, forwarding the incoming
+ * request's cookie so the route authenticates as the current user.
+ *
+ * List prefetches go through the route (rather than the data layer) when the
+ * payload carries `Date` fields: `NextResponse.json` serializes them to the
+ * string wire shape the client caches via `requestJson`, so the
+ * server-hydrated entry byte-matches the client-fetched one through
+ * dehydration. Calling the data layer directly would cache raw `Date` objects
+ * and drift from that wire shape. Mirrors the settings/subscription prefetch.
+ */
+export async function prefetchInternalJson(path: string): Promise {
+ const cookie = (await headers()).get('cookie')
+ // boundary-raw-fetch: server-side RSC prefetch forwarding the session cookie to an internal API route; requestJson is client-only and cannot run here
+ const response = await fetch(`${getInternalApiBaseUrl()}${path}`, {
+ headers: cookie ? { cookie } : {},
+ })
+ if (!response.ok) {
+ throw new Error(`Prefetch failed for ${path}: ${response.status}`)
+ }
+ return response.json() as Promise
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts b/apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts
new file mode 100644
index 00000000000..d031c0648ef
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts
@@ -0,0 +1,169 @@
+/**
+ * @vitest-environment node
+ */
+import { QueryClient } from '@tanstack/react-query'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const { mockPrefetchInternalJson } = vi.hoisted(() => ({
+ mockPrefetchInternalJson: vi.fn(),
+}))
+
+vi.mock('@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch', () => ({
+ prefetchInternalJson: mockPrefetchInternalJson,
+}))
+
+vi.mock('@/components/emcn', () => ({
+ toast: { success: vi.fn(), error: vi.fn() },
+}))
+
+import { prefetchFilesBrowser } from '@/app/workspace/[workspaceId]/files/prefetch'
+import { prefetchHomeLists } from '@/app/workspace/[workspaceId]/home/prefetch'
+import { prefetchKnowledgeBases } from '@/app/workspace/[workspaceId]/knowledge/prefetch'
+import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
+import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
+import { tableKeys } from '@/hooks/queries/tables'
+import { folderKeys } from '@/hooks/queries/utils/folder-keys'
+import { workspaceFileFolderKeys } from '@/hooks/queries/workspace-file-folders'
+import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
+
+const WORKSPACE_ID = 'ws-123'
+
+function makeClient() {
+ return new QueryClient({ defaultOptions: { queries: { retry: false } } })
+}
+
+describe('workspace list prefetches', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('prefetchKnowledgeBases', () => {
+ it('primes the exact key useKnowledgeBasesQuery reads and unwraps data', async () => {
+ const bases = [{ id: 'kb-1' }]
+ mockPrefetchInternalJson.mockResolvedValue({ data: bases })
+ const client = makeClient()
+
+ await prefetchKnowledgeBases(client, WORKSPACE_ID)
+
+ expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
+ `/api/knowledge?workspaceId=${WORKSPACE_ID}&scope=active`
+ )
+ expect(client.getQueryData(knowledgeKeys.list(WORKSPACE_ID, 'active'))).toEqual(bases)
+ })
+ })
+
+ describe('prefetchTables', () => {
+ it('primes the exact key useTablesList reads and unwraps data.tables', async () => {
+ const tables = [{ id: 't-1' }]
+ mockPrefetchInternalJson.mockResolvedValue({ data: { tables } })
+ const client = makeClient()
+
+ await prefetchTables(client, WORKSPACE_ID)
+
+ expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
+ `/api/table?workspaceId=${WORKSPACE_ID}&scope=active`
+ )
+ expect(client.getQueryData(tableKeys.list(WORKSPACE_ID, 'active'))).toEqual(tables)
+ })
+ })
+
+ describe('prefetchFilesBrowser', () => {
+ it('primes both file + folder keys the client hooks read', async () => {
+ const files = [{ id: 'f-1' }]
+ const folders = [{ id: 'folder-1' }]
+ mockPrefetchInternalJson.mockImplementation(async (path: string) =>
+ path.includes('/folders') ? { folders } : { success: true, files }
+ )
+ const client = makeClient()
+
+ await prefetchFilesBrowser(client, WORKSPACE_ID)
+
+ expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
+ `/api/workspaces/${WORKSPACE_ID}/files?scope=active`
+ )
+ expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
+ `/api/workspaces/${WORKSPACE_ID}/files/folders?scope=active`
+ )
+ expect(client.getQueryData(workspaceFilesKeys.list(WORKSPACE_ID, 'active'))).toEqual(files)
+ expect(client.getQueryData(workspaceFileFolderKeys.list(WORKSPACE_ID, 'active'))).toEqual(
+ folders
+ )
+ })
+
+ it('caches an empty file list when the route reports failure', async () => {
+ mockPrefetchInternalJson.mockImplementation(async (path: string) =>
+ path.includes('/folders') ? { folders: [] } : { success: false, files: [] }
+ )
+ const client = makeClient()
+
+ await prefetchFilesBrowser(client, WORKSPACE_ID)
+
+ expect(client.getQueryData(workspaceFilesKeys.list(WORKSPACE_ID, 'active'))).toEqual([])
+ })
+ })
+
+ describe('prefetchHomeLists', () => {
+ it('primes folder + file keys, mapping folder rows to the client shape', async () => {
+ const folderRow = {
+ id: 'folder-1',
+ name: 'Docs',
+ userId: 'u-1',
+ workspaceId: WORKSPACE_ID,
+ parentId: null,
+ color: null,
+ isExpanded: true,
+ locked: false,
+ sortOrder: 0,
+ createdAt: '2026-01-01T00:00:00.000Z',
+ updatedAt: '2026-01-02T00:00:00.000Z',
+ archivedAt: null,
+ }
+ const files = [{ id: 'f-1' }]
+ mockPrefetchInternalJson.mockImplementation(async (path: string) =>
+ path.startsWith('/api/folders') ? { folders: [folderRow] } : { success: true, files }
+ )
+ const client = makeClient()
+
+ await prefetchHomeLists(client, WORKSPACE_ID)
+
+ expect(mockPrefetchInternalJson).toHaveBeenCalledWith(
+ `/api/folders?workspaceId=${WORKSPACE_ID}&scope=active`
+ )
+ const cachedFolders = client.getQueryData(folderKeys.list(WORKSPACE_ID, 'active')) as Array<{
+ id: string
+ color: string
+ createdAt: Date
+ }>
+ expect(cachedFolders).toHaveLength(1)
+ expect(cachedFolders[0].color).toBe('#6B7280')
+ expect(cachedFolders[0].createdAt).toBeInstanceOf(Date)
+ expect(client.getQueryData(workspaceFilesKeys.list(WORKSPACE_ID, 'active'))).toEqual(files)
+ })
+ })
+
+ describe('graceful failure', () => {
+ it.each([
+ [
+ 'prefetchKnowledgeBases',
+ prefetchKnowledgeBases,
+ knowledgeKeys.list(WORKSPACE_ID, 'active'),
+ ],
+ ['prefetchTables', prefetchTables, tableKeys.list(WORKSPACE_ID, 'active')],
+ ['prefetchHomeLists', prefetchHomeLists, folderKeys.list(WORKSPACE_ID, 'active')],
+ [
+ 'prefetchFilesBrowser',
+ prefetchFilesBrowser,
+ workspaceFilesKeys.list(WORKSPACE_ID, 'active'),
+ ],
+ ] as const)(
+ '%s does not throw when the fetcher rejects (page still renders, client refetches)',
+ async (_name, prefetch, queryKey) => {
+ mockPrefetchInternalJson.mockRejectedValue(new Error('500'))
+ const client = makeClient()
+
+ await expect(prefetch(client, WORKSPACE_ID)).resolves.toBeUndefined()
+ expect(client.getQueryData(queryKey)).toBeUndefined()
+ }
+ )
+ })
+})
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/page.tsx b/apps/sim/app/workspace/[workspaceId]/tables/page.tsx
index 15a016a25a2..0e9390a5d95 100644
--- a/apps/sim/app/workspace/[workspaceId]/tables/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/tables/page.tsx
@@ -1,6 +1,9 @@
import { Suspense } from 'react'
+import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import type { Metadata } from 'next'
+import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import TablesLoading from '@/app/workspace/[workspaceId]/tables/loading'
+import { prefetchTables } from '@/app/workspace/[workspaceId]/tables/prefetch'
import { Tables } from './tables'
export const metadata: Metadata = {
@@ -13,10 +16,17 @@ export const metadata: Metadata = {
* fallback renders the real chrome so a suspend never shows a blank frame; the
* route-level `loading.tsx` covers the navigation/chunk-load transition.
*/
-export default function TablesPage() {
+export default async function TablesPage({ params }: { params: Promise<{ workspaceId: string }> }) {
+ const { workspaceId } = await params
+
+ const queryClient = getQueryClient()
+ await prefetchTables(queryClient, workspaceId)
+
return (
- }>
-
-
+
+ }>
+
+
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts b/apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts
new file mode 100644
index 00000000000..60d6a79a735
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts
@@ -0,0 +1,26 @@
+import type { QueryClient } from '@tanstack/react-query'
+import type { TableDefinition } from '@/lib/table'
+import { prefetchInternalJson } from '@/app/workspace/[workspaceId]/lib/prefetch-internal-fetch'
+import { tableKeys } from '@/hooks/queries/tables'
+
+/**
+ * Prefetches the workspace's tables list under the same query key the client
+ * `useTablesList` hook uses (scope `active`), so the list paints populated on
+ * first render.
+ *
+ * Table definitions carry `Date` fields, so the list goes through the
+ * `/api/table` route and caches the serialized wire shape — see
+ * {@link prefetchInternalJson}.
+ */
+export async function prefetchTables(queryClient: QueryClient, workspaceId: string): Promise {
+ await queryClient.prefetchQuery({
+ queryKey: tableKeys.list(workspaceId, 'active'),
+ queryFn: async () => {
+ const response = await prefetchInternalJson<{ data: { tables: TableDefinition[] } }>(
+ `/api/table?workspaceId=${workspaceId}&scope=active`
+ )
+ return response.data.tables
+ },
+ staleTime: 30 * 1000,
+ })
+}
diff --git a/apps/sim/hooks/queries/folders.ts b/apps/sim/hooks/queries/folders.ts
index f0d8913cd05..34b695374fd 100644
--- a/apps/sim/hooks/queries/folders.ts
+++ b/apps/sim/hooks/queries/folders.ts
@@ -25,7 +25,15 @@ import type { WorkflowFolder } from '@/stores/folders/types'
const logger = createLogger('FolderQueries')
-function mapFolder(folder: FolderApi): WorkflowFolder {
+export const FOLDER_LIST_STALE_TIME = 60 * 1000
+
+/**
+ * Maps a wire folder row to the client `WorkflowFolder` shape (string dates →
+ * `Date`, color default). Exported so the server-side home prefetch produces
+ * the exact cached value `useFolders` stores, keeping the hydrated entry in
+ * sync with a client fetch.
+ */
+export function mapFolder(folder: FolderApi): WorkflowFolder {
return {
id: folder.id,
name: folder.name,