Next.js SSR Authentication#
Overview#
Dify's web frontend (v1.15.0+) uses Next.js App Router with Server-Side Rendering (SSR). During SSR, the Next.js Node process makes direct HTTP calls to the Dify API backend β distinct from browser-side API calls. This means the web container needs its own server-side API URL configuration, and the auth token-refresh flow runs server-side rather than in the browser.
The core SSR auth flow:
- An RSC (React Server Component) detects a 401 from the API and redirects to
GET /auth/refresh /auth/refreshposts to the backend/refresh-tokenendpoint using the request's cookies- On success, it issues a 303 redirect back to the original page with refreshed
Set-Cookieheaders; on failure, it redirects to/signin
Key files:
| File | Role |
|---|---|
web/config/server.ts | Resolves SERVER_CONSOLE_API_URL β SERVER_CONSOLE_API_PREFIX for SSR fetches |
web/service/console/server | Server oRPC client, resolveServerConsoleApiUrl() |
web/app/auth/refresh/route.ts | SSR token refresh + redirect handler |
web/hooks/use-timestamp.ts | Account timezone hook (source of v1.15.0 public-route regression) |
web/env.ts | Server env schema: declares SERVER_CONSOLE_API_URL and CONSOLE_API_URL |
web/docker/entrypoint.sh | Maps Docker env vars to NEXT_PUBLIC_* equivalents at container start |
SERVER_CONSOLE_API_URL β SSR-Specific Environment Variable#
Because the Next.js server process needs to reach the Dify API directly (not through the browser-facing public URL), v1.15.0 introduced a dedicated server-side env var: SERVER_CONSOLE_API_URL.
Resolution logic in web/config/server.ts :
SERVER_CONSOLE_API_PREFIX = (SERVER_CONSOLE_API_URL || CONSOLE_API_URL) + "/console/api"
SERVER_CONSOLE_API_URL takes precedence; if absent, CONSOLE_API_URL (the browser-facing URL, also a server env var) is used as a fallback. resolveServerConsoleApiUrl() in web/service/console/server uses this prefix to construct absolute URLs for SSR fetches . If neither variable resolves, the function returns null and the auth refresh route handler falls back to /signin .
Why it matters in Kubernetes/Docker: The web container's Node process must reach the API container over the internal network. External hostnames (used by CONSOLE_API_URL for browser access) are often unresolvable inside the cluster. Set SERVER_CONSOLE_API_URL to the internal DNS name, e.g.:
SERVER_CONSOLE_API_URL=http://dify-api-svc.dify.svc.cluster.local:5001
Leaving this unset while CONSOLE_API_URL points to an external hostname is the most common cause of net::ERR_CONNECTION_CLOSED and silent SSR refresh failures .
Not set by entrypoint.sh: web/docker/entrypoint.sh maps CONSOLE_API_URL β NEXT_PUBLIC_API_PREFIX but does not set SERVER_CONSOLE_API_URL β it must be injected separately . Both vars are declared as server-only (no NEXT_PUBLIC_ prefix) in web/env.ts .
useTimestamp() Hook Regression (v1.15.0)#
The most impactful v1.15.0 regression: anonymous users on public Web App routes (/chat/{token}, /chatbot/{token}, /completion/{token}, /workflow/{token}, /agent/{token}) were being redirected to /signin.
Root cause: useTimestamp() fetches GET /console/api/account/profile to retrieve the user's account timezone. In v1.15.0, this query ran unconditionally (enabled whenever no explicit timezone override was passed). Public Web App routes have no console session, so the request returns 401 β triggers the refresh flow β refresh fails β redirect to /signin.
Call chain: ChatWithHistoryWrap β useChat() β useTimestamp() β userProfileQueryOptions() β GET /console/api/account/profile β 401
Fix (PR #38116): Added web/utils/is-public-webapp-route.ts which checks if the current pathname starts with agent, chat, chatbot, completion, or workflow. The useTimestamp() hook now gates the query so it only runs outside public webapp routes, and falls back to getBrowserTimezone() on public routes . The current main branch code already reflects this fix .
Related issues: #38043, #38111, #38956, #38457 β all stem from the same root cause. Fix was also confirmed resolved in PR #37915 (a preceding partial fix) and fully closed by PR #38116.
Workarounds for unpatched v1.15.0 deployments:
- Downgrade only the web container to v1.14.2 while keeping API/worker on v1.15.0
- For reverse-proxy setups: stub
GET /console/api/account/profileto return a synthetic 200 for requests with a share-pageRefererheader
Redirect Loop Debugging#
Redirect loops present as the browser cycling between /signin, /auth/refresh, and the original route. Two distinct causes:
1. useTimestamp() issuing console API calls from public routes (v1.15.0 regression, fixed in PR #38116): The loop is public route β 401 β /auth/refresh β /signin β .... Confirm by checking browser DevTools for GET /console/api/account/profile on share pages.
2. SERVER_CONSOLE_API_URL unreachable from the web container: The refresh handler POSTs to the internal API; if it fails (connection error or non-2xx), it redirects to /signin. If /signin also triggers SSR profile checks that fail, a loop follows. Confirm by checking web container logs for fetch errors or ECONNREFUSED at startup.
The /auth/refresh route handler has explicit loop prevention β it blocks any redirect_url that points back to /auth/refresh itself . But this guard doesn't help when the loop runs through /signin instead.
Checklist:
- Is
SERVER_CONSOLE_API_URLset and resolvable from inside the web container? (curlfrom within the pod) - Is the web container running v1.15.0 with the unpatched
useTimestamp()hook? - Does the loop affect only public
/chat//chatbotroutes (hook bug) or also console routes (env var issue)?