Skip to content

Commit c354d84

Browse files
committed
feat: add API_URL_SSR to override api url during ssr
1 parent 8fe898a commit c354d84

2 files changed

Lines changed: 50 additions & 5 deletions

File tree

apps/public/content/docs/self-hosting/environment-variables.mdx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,29 @@ Public API URL exposed to the browser. Used by the dashboard frontend and API se
129129
API_URL=https://analytics.example.com/api
130130
```
131131

132+
### API_URL_SSR
133+
134+
**Type**: `string`
135+
**Required**: No
136+
**Default**: Same as `API_URL`
137+
138+
Internal API URL used **only** for the dashboard's server-side rendering (SSR) requests. Set this when the dashboard server can't resolve or reach the public `API_URL` and needs an internal address instead — for example a Docker Compose service name or a Kubernetes cluster-internal address.
139+
140+
The browser always keeps using the public `API_URL`; only requests made from the dashboard server during SSR use `API_URL_SSR`. If unset, SSR falls back to `API_URL`.
141+
142+
**Example**:
143+
```bash
144+
# Docker Compose: reach the API service directly over the internal network
145+
API_URL_SSR=http://api:3000
146+
147+
# Kubernetes: cluster-internal service DNS
148+
API_URL_SSR=http://openpanel-api.default.svc.cluster.local:3000
149+
```
150+
151+
<Callout>
152+
This only affects the dashboard's server-side requests. It does not change the API service itself, CORS, or any browser-facing URLs — those still use `API_URL`.
153+
</Callout>
154+
132155
### DASHBOARD_URL
133156

134157
**Type**: `string`

apps/start/src/integrations/tanstack-query/root-provider.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,35 @@ function shouldRetryQuery(failureCount: number, error: unknown) {
2121
}
2222

2323
function handleUnauthorized(error: unknown) {
24-
if (typeof window === 'undefined') return;
25-
if (!(error instanceof TRPCClientError)) return;
26-
if (error.data?.httpStatus !== 401) return;
27-
if (window.location.pathname.startsWith('/login')) return;
24+
if (typeof window === 'undefined') {
25+
return;
26+
}
27+
if (!(error instanceof TRPCClientError)) {
28+
return;
29+
}
30+
if (error.data?.httpStatus !== 401) {
31+
return;
32+
}
33+
if (window.location.pathname.startsWith('/login')) {
34+
return;
35+
}
2836
// Hard navigation tears down in-flight refetches and WS subscriptions so
2937
// the stale tab stops hammering the API after the session is gone.
3038
window.location.assign('/login');
3139
}
3240

41+
// Resolve the tRPC base URL per environment. During SSR the server can reach
42+
// the API over an internal address (e.g. a Docker/K8s service name) that the
43+
// browser can't resolve, so `API_URL_SSR` overrides the public `apiUrl`
44+
// server-side only. The client always uses the public `apiUrl` it was given.
45+
const getSsrApiUrlOverride = createIsomorphicFn()
46+
.server(() => {
47+
console.log('ENVS', process.env);
48+
console.log('API_URL_SSR', process.env.API_URL_SSR);
49+
return process.env.API_URL_SSR || undefined;
50+
})
51+
.client(() => undefined);
52+
3353
export const getIsomorphicHeaders = createIsomorphicFn()
3454
.server(() => {
3555
const headers = getRequestHeaders();
@@ -50,11 +70,13 @@ export const getIsomorphicHeaders = createIsomorphicFn()
5070

5171
// Create a function that returns a tRPC client with optional cookies
5272
export function createTRPCClientWithHeaders(apiUrl: string) {
73+
const baseUrl = getSsrApiUrlOverride() || apiUrl;
74+
console.log('baseUrl', baseUrl);
5375
return createTRPCClient<AppRouter>({
5476
links: [
5577
httpLink({
5678
transformer: superjson,
57-
url: `${apiUrl}/trpc`,
79+
url: `${baseUrl}/trpc`,
5880
headers: () => getIsomorphicHeaders(),
5981
fetch: async (url, options) => {
6082
try {

0 commit comments

Comments
 (0)