Fix/status page forwarded host routing#1977
Fix/status page forwarded host routing#1977aiokaizen wants to merge 4 commits intoopenstatusHQ:mainfrom
Conversation
aiokaizen
commented
Mar 13, 2026
- Fixes custom-domain routing for self-hosted status pages behind a reverse proxy.
- The status-page app was reading x-forwarded-host, but then still derived subdomain from url.host.
- In self-hosted Docker deployments, url.host is the container host (0.0.0.0:3000 / localhost:3000), not the public domain.
- That caused the resolved routing prefix to be overwritten with the container host, breaking custom-domain routing.
- Result: requests for domains like status.pinbridge.io could fall back to the public root/theme-builder flow instead of the intended status page.
- This patch introduces effectiveHost = x-forwarded-host ?? url.host and uses it consistently for:
- hostname splitting
- subdomain detection
- .vercel.app checks
- Updated files:
- apps/status-page/src/proxy.ts
- apps/status-page/src/lib/domain.ts
- This is especially important for self-hosted setups behind nginx or another reverse proxy that forwards the public host.
- No version changes included.
|
@aiokaizen is attempting to deploy a commit to the OpenStatus Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR primarily fixes self-hosted status-page domain routing behind reverse proxies by consistently using x-forwarded-host (when present) instead of the container host, and it additionally introduces broader self-host wiring for checker/workflows, Redis, and public URL configuration.
Changes:
- Fix status-page routing by introducing
effectiveHost = x-forwarded-host ?? url.hostand using it for hostname/subdomain detection. - Add self-host mode support across API/server/workflows/checker (configurable checker/workflows base URLs, region handling, and direct-dispatch fallbacks when Cloud Tasks aren’t available).
- Update Docker compose + env examples to run Redis + redis-http shim and a local checker service, and introduce a dashboard public URL helper used for absolute redirect/callback URLs.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/api/src/router/checker.ts | Adds self-host checker routing helpers and uses them for ping/tcp/dns tests + trigger URL generation. |
| packages/api/package.json | Version bump. |
| docker-compose.yaml | Adds Redis + redis-http shim, local checker service, and self-host env wiring across services. |
| docker-compose.github-packages.yaml | Mirrors compose changes for GitHub Packages deployment. |
| apps/workflows/src/env.ts | Adds SELF_HOST and base URL/region envs for self-host. |
| apps/workflows/src/cron/monitor.ts | Skips Cloud Tasks usage in self-host/missing-config scenarios and threads context through task creation. |
| apps/workflows/src/cron/checker.ts | Adds direct checker dispatch path when self-host or Cloud Tasks config missing. |
| apps/status-page/src/proxy.ts | Uses effectiveHost for hostname/subdomain routing decisions. |
| apps/status-page/src/lib/domain.ts | Uses effectiveHost for custom-domain parsing and routing decisions. |
| apps/status-page/package.json | Version bump. |
| apps/server/src/routes/v1/check/http/post.ts | Supports self-host checker base URL/region for manual HTTP checks. |
| apps/server/src/routes/slack/oauth.ts | Makes dashboard redirect base configurable via env and trims trailing slash. |
| apps/server/src/libs/checker/utils.ts | Uses self-host checker base URL when generating checker URLs. |
| apps/server/package.json | Version bump. |
| apps/dashboard/src/lib/public-url.ts | New helper for consistent dashboard public URL construction. |
| apps/dashboard/src/components/data-table/billing/data-table.tsx | Uses dashboard public URL for Stripe success/cancel URLs. |
| apps/dashboard/src/app/(dashboard)/settings/integrations/slack-card.tsx | Switches Slack install redirect base to dashboard public URL helper. |
| apps/dashboard/src/app/(dashboard)/settings/general/page.tsx | Uses dashboard public URL for invite link base. |
| apps/dashboard/src/app/(dashboard)/settings/billing/client.tsx | Uses dashboard public URL helper. |
| apps/dashboard/src/app/(dashboard)/notifications/client.tsx | Uses dashboard public URL for PagerDuty redirect URL base. |
| apps/dashboard/package.json | Version bump. |
| apps/checker/checker/update.go | Adds direct HTTP fallback for posting status updates to workflows when self-host. |
| .env.docker.example | Updates self-host defaults (Redis shim, checker/workflows routing). |
| .env.docker-lightweight.example | Adds self-host routing + Redis shim envs for lightweight setup. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| function isSelfHost() { | ||
| return process.env.SELF_HOST === "true"; | ||
| } | ||
|
|
||
| function getCheckerBaseUrl() { | ||
| return (process.env.CHECKER_BASE_URL || "http://checker:8080").replace( | ||
| /\/$/, | ||
| "", | ||
| ); | ||
| } | ||
|
|
||
| function getCheckerRegion(region: string) { | ||
| if (!isSelfHost()) { | ||
| return region; | ||
| } | ||
|
|
||
| return process.env.CHECKER_REGION || "ams"; | ||
| } |
| const cloudTaskContext = getCloudTaskContext("workflow"); | ||
| if (!cloudTaskContext) { | ||
| console.log( | ||
| "Skipping monitor lifecycle workflow: Cloud Tasks are unavailable in self-host mode.", |
| function isSelfHost() { | ||
| return process.env.SELF_HOST === "true"; | ||
| } | ||
|
|
||
| function getCheckerBaseUrl() { | ||
| return (process.env.CHECKER_BASE_URL || "http://checker:8080").replace( | ||
| /\/$/, | ||
| "", | ||
| ); | ||
| } |
| function generateUrl({ row }: { row: z.infer<typeof selectMonitorSchema> }) { | ||
| if (isSelfHost()) { | ||
| return `${getCheckerBaseUrl()}/checker/${row.jobType}?monitor_id=${row.id}`; | ||
| } |
| process.env.NODE_ENV === "production" | ||
| ? "https://api.openstatus.dev" | ||
| : "http://localhost:3000"; | ||
| const SERVER_URL = getDashboardPublicUrl(); |
| c: Context, | ||
| ) { | ||
| const timestamp = Date.now(); | ||
| const selfHostRegion = env().CHECKER_REGION as Region; |
| req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, payloadBuf) | ||
| if err != nil { | ||
| log.Ctx(ctx).Error().Err(err).Msg("error while creating update request") | ||
| return err | ||
| } | ||
| req.Header.Set("Authorization", basic) | ||
| req.Header.Set("Content-Type", "application/json") | ||
|
|
||
| res, err := http.DefaultClient.Do(req) | ||
| if err != nil { | ||
| log.Ctx(ctx).Error().Err(err).Msg("error while posting update status directly") | ||
| return err | ||
| } |
| { | ||
| "name": "@openstatus/api", | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", |
| - "8079:80" | ||
| environment: | ||
| - SRH_MODE=env | ||
| - SRH_TOKEN=${UPSTASH_REDIS_REST_TOKEN:-replace-with-a-long-random-secret} |
| - SRH_MODE=env | ||
| - SRH_TOKEN=${UPSTASH_REDIS_REST_TOKEN:-replace-with-a-long-random-secret} |