SSR Data Fetching#
Overview#
Dify's Next.js frontend (App Router) uses TanStack Query's HydrationBoundary to prefetch API data on the server and stream it to the client, eliminating redundant client-side API calls on initial page load. Console calls use a single consoleQuery from @/service/console with environment-aware transport selection via DynamicLink, which automatically selects the browser HTTP adapter or a registered server HTTP adapter. The key entry point is CommonLayoutHydrationBoundary in web/app/(commonLayout)/hydration-boundary.tsx, which prefetches the user profile, current workspace, RBAC permissions, and workspace features before any client component renders.
How It Works#
RSC (Server Component)
βββ getQueryClient() β fresh QueryClient per request
βββ queryClient.query(...) β fires SSR API requests
βββ queryClient.query(...).catch(noop) β prefetch-style (non-critical)
βββ dehydrate(queryClient) β serializes cache
βββ <HydrationBoundary state={β¦}> β passes to client
βββ client components use useSuspenseQuery β no extra fetch
CommonLayoutHydrationBoundarycallsgetQueryClient()to create a newQueryClientwith a 5-minutestaleTimeand TanStack Query Advanced SSR defaults that include pending queries in dehydrated state:shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending'.CommonLayoutHydrationBoundarycallsqueryClient.query(serverUserProfileQueryOptions())for the profile (critical) andqueryClient.query(...).catch(noop)for current workspace, RBAC permissions, and workspace features (non-critical) in parallel . Non-critical queries use.catch(noop)to consume rejected promises at fire-and-forget boundaries while keeping transport-owned error feedback unchanged. Server cookies and CSRF headers are resolved per request automatically throughDynamicLink, without explicit context passing.- The populated QueryClient is dehydrated and passed into
<HydrationBoundary>. Client components callinguseSuspenseQuerywith the same query key find the data already populated β no extra round trip. - If the API returns a
401, the component redirects to/auth/refresh?redirect_url=β¦for SSR token renewal .
Server-Side API URL Resolution#
SSR requests go node-to-node (Next.js process β API backend), not through the browser. Two env vars control this :
| Variable | Purpose |
|---|---|
SERVER_CONSOLE_API_URL | Internal URL for server-to-server requests (e.g., http://api:5001) |
CONSOLE_API_URL | Browser-facing public URL; used as fallback when SERVER_CONSOLE_API_URL is unset |
web/config/server.ts computes SERVER_CONSOLE_API_PREFIX:
SERVER_CONSOLE_API_PREFIX = (SERVER_CONSOLE_API_URL || CONSOLE_API_URL) + "/console/api"
resolveServerConsoleApiUrl(pathname) in web/service/console/server builds the full URL from this prefix. If the prefix resolves to null (both vars unset), CommonLayoutHydrationBoundary skips SSR prefetching β no crash, but no hydration .
In Docker/Kubernetes, always set SERVER_CONSOLE_API_URL to the internal service address. Using only CONSOLE_API_URL with an external hostname causes silent SSR failures because the Node process can't resolve external DNS inside the cluster .
Unified oRPC Client Pattern#
The unified console client uses consoleQuery from @/service/console in both Server and Client Components:
import { consoleQuery } from '@/service/console'
import { resolveServerConsoleApiUrl } from '@/service/console/server'
// In a Server Component:
const accountProfileUrl = resolveServerConsoleApiUrl('/account/profile')
if (accountProfileUrl) {
await Promise.all([
queryClient.query(serverUserProfileQueryOptions()),
queryClient
.query(
consoleQuery.workspaces.current.summary.get.queryOptions({
retry: false,
}),
)
.catch(noop),
queryClient
.query(
consoleQuery.workspaces.current.rbac.myPermissions.get.queryOptions({
retry: false,
}),
)
.catch(noop),
queryClient
.query(
consoleQuery.features.get.queryOptions({
retry: false,
}),
)
.catch(noop),
])
}
DynamicLink selects the browser HTTP adapter or a registered server HTTP adapter automatically. Server cookies and CSRF headers are resolved per request without explicit context passing. The browser adapter uses NEXT_PUBLIC_API_PREFIX and the browser's cookie jar. The server adapter uses SERVER_CONSOLE_API_PREFIX and extracts request identity from Next.js headers() and cookies() at execution time.
Workspace Features Prefetch#
Workspace features (including flags like enable_skill) are prefetched via consoleQuery.features.get() to ensure SSR and client hydration use the same feature flag values. This prevents UI elements (like the Skills feature) from appearing only after the client-side features request completes when the server prefetch succeeds. The features query uses the request-scoped QueryClient to prevent value leakage between server requests β a failed features prefetch on one request does not inherit stale enabled flags from a prior successful request. Like workspace summary and RBAC permissions, the features query is non-blocking (.catch(noop)) so a failed features request does not block rendering; the client can recover normally.
Key Files#
| File | Role |
|---|---|
web/app/(commonLayout)/hydration-boundary.tsx | CommonLayoutHydrationBoundary β prefetches profile + workspace + RBAC + features using consoleQuery from @/service/console |
web/service/console | Unified console oRPC client with environment-aware transport |
web/service/console/server | Server HTTP adapter registration and URL resolution |
web/config/server.ts | Computes SERVER_CONSOLE_API_PREFIX from env vars |
web/app/get-query-client.ts | getQueryClient() β application QueryClient factory with Advanced SSR defaults |
web/features/system-features/server.ts | Exports: getOptionalSystemFeatures(), getSystemFeatures(), dehydrateSystemFeatures() |
web/features/account-profile/server.ts | serverUserProfileQueryOptions() β example SSR query option pattern |
System Features Server API#
web/features/system-features/server.ts provides server-side access to System Features (branding, feature flags, deployment edition) with request-boundary-aware query operations:
-
getOptionalSystemFeatures()β ReturnsPromise<SystemFeatures | undefined>. Awaitsconnection(), checks if query state has an error with no data and returnsundefinedearly. Otherwise callsqueryClient.query(systemFeaturesServerQueryOptions())with a.catch(() => undefined)handler that returnsundefinedon error. Uses explicitstaleTime: 'static'in query options to accept any successful request-local snapshot even after invalidation. Safe for optional features like metadata and integration inclusion. -
getSystemFeatures()β ReturnsPromise<SystemFeatures>. Awaitsconnection()and directly callsqueryClient.query(systemFeaturesServerQueryOptions())without error handling. Throws on fetch failure. Use for required route gates like KnowledgeFS enablement checks. -
dehydrateSystemFeatures()β Returns dehydrated state by callingdehydrate(getRequestQueryClient()). Use in layouts to serialize System Features into<HydrationBoundary>state.
All functions respect the Next.js connection() boundary to ensure they run after the request boundary is established, preventing backend requests during static prerendering. systemFeaturesServerQueryOptions is now a private implementation detail used internally by these functions.
Query Semantics#
The migration uses query() instead of fetchQuery()/prefetchQuery()/ensureQueryData() to align with TanStack Query 5.102.8+ semantics:
- Fetch-style reads use
query()and preserve rejection (e.g., profile fetch inCommonLayoutHydrationBoundary). - Prefetch-style reads use
query().catch(noop)to consume rejected promises at fire-and-forget boundaries while keeping transport-owned error feedback unchanged (e.g., workspace summary, RBAC permissions, and workspace features). - Ensure-style cache snapshots use
staleTime: 'static'to accept any successful request-local snapshot even after invalidation (e.g.,systemFeaturesServerQueryOptions).
No new toast behavior or compatibility layer is introduced.
Related PRs#
- feat(web): add server oRPC client β introduced
web/service/server.tsandserverConsoleQuery - fix: configure server console api url β added
SERVER_CONSOLE_API_URLenv var and fallback logic - fix(web): gate system features requests by connection β added request boundary and soft/hard prefetch operations
- refactor(query): migrate imperative APIs to query semantics β migrated to TanStack Query 5.102.8+ query semantics
- refactor(console): unify environment-aware oRPC clients and query policies β unified console clients behind
@/service/consolewithDynamicLinktransport selection