First off - thanks for Rybbit. The self-hosting story is genuinely good, and getting an instance up behind our own ingress was painless.
I set up the Google Search Console integration on a self-hosted instance and got everything working on the backend, but there's no way to reach it in the UI. The Integrations tab is hidden unless IS_CLOUD is true:
client/src/components/SiteSettings/SiteSettings.tsx:109
{ key: "integrations", label: t("Integrations"), icon: Plug, hidden: !IS_CLOUD },
client/src/components/SiteSettings/SiteSettings.tsx:186
{activeTab === "integrations" && IS_CLOUD && <IntegrationsTab disabled={disabled} />}
What made me think this might be unintentional is that the server side isn't gated at all — there's no IS_CLOUD check anywhere in server/src/api/gsc/. With GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI set on the backend, the routes work fine on self-host:
$ curl -s -w ' %{http_code}\n' https://<our-instance>/api/gsc/callback
{"error":"Missing code or state parameter"} 400
$ curl -s -w ' %{http_code}\n' https://<our-instance>/api/sites/1/gsc/connect
{"error":"Forbidden"} 403
Both handlers are running — 403 is the admin-access check on an unauthenticated request, and neither returns 500 Google OAuth not configured. So the feature appears fully functional on self-host; it's just that the only entry point to it is hidden.
This is on v2.8.0, and the gate is still present on master as of 806e368.
For contrast, R2 replay storage is gated deliberately and consistently — r2StorageService short-circuits on IS_CLOUD server-side, and the env vars live only in docker-compose.cloud.yml. GSC has the cloud-only compose vars but no server-side gate, which is what made the client-side hiding feel more like an oversight than a product decision.
Describe the solution you'd like
Show the Integrations tab on self-hosted when Google OAuth is actually configured, rather than keying it off IS_CLOUD. Something like exposing a googleOAuthConfigured boolean from the backend config endpoint (true when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are both set) and using that for hidden instead. That keeps the tab out of the way for the majority of self-hosters who haven't set up an OAuth client, while letting those who have actually reach it.
If it's simpler, gating on IS_CLOUD || googleOAuthConfigured would preserve today's cloud behaviour exactly.
I'm happy to open a PR if you'd like — just wanted to check first whether the current gating is deliberate, since I might be missing a reason it's cloud-only (quota or verification constraints on the OAuth client, maybe?). Entirely possible there's context I don't have.
Additional context
Worth flagging separately, since anyone who does get the tab enabled will hit it: the GSC error redirects don't land anywhere useful. callback.ts redirects failures to ${BASE_URL}/error, but the client app has no /error route, so the user gets a generic 404 with the message dropped. The "no properties found" branch uses ${CLIENT_URL}, which isn't defined in either compose file or .env.example, producing a Location of undefined/error?.... Your own FEATURE_AUDIT.md:561 documents this. The success path is fine.
Relatedly, both connect.ts:33 and callback.ts:63 fall back to ${process.env.SERVER_URL}/api/gsc/callback when GOOGLE_REDIRECT_URI is unset, but SERVER_URL doesn't seem to be defined anywhere in the repo — so the fallback yields a literal undefined/api/gsc/callback. Setting GOOGLE_REDIRECT_URI explicitly works around it. Happy to split these into their own issue if you'd rather keep this one focused.
First off - thanks for Rybbit. The self-hosting story is genuinely good, and getting an instance up behind our own ingress was painless.
I set up the Google Search Console integration on a self-hosted instance and got everything working on the backend, but there's no way to reach it in the UI. The Integrations tab is hidden unless
IS_CLOUDis true:What made me think this might be unintentional is that the server side isn't gated at all — there's no
IS_CLOUDcheck anywhere inserver/src/api/gsc/. WithGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_REDIRECT_URIset on the backend, the routes work fine on self-host:Both handlers are running —
403is the admin-access check on an unauthenticated request, and neither returns500 Google OAuth not configured. So the feature appears fully functional on self-host; it's just that the only entry point to it is hidden.This is on
v2.8.0, and the gate is still present onmasteras of806e368.For contrast, R2 replay storage is gated deliberately and consistently —
r2StorageServiceshort-circuits onIS_CLOUDserver-side, and the env vars live only indocker-compose.cloud.yml. GSC has the cloud-only compose vars but no server-side gate, which is what made the client-side hiding feel more like an oversight than a product decision.Describe the solution you'd like
Show the Integrations tab on self-hosted when Google OAuth is actually configured, rather than keying it off
IS_CLOUD. Something like exposing agoogleOAuthConfiguredboolean from the backend config endpoint (true whenGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETare both set) and using that forhiddeninstead. That keeps the tab out of the way for the majority of self-hosters who haven't set up an OAuth client, while letting those who have actually reach it.If it's simpler, gating on
IS_CLOUD || googleOAuthConfiguredwould preserve today's cloud behaviour exactly.I'm happy to open a PR if you'd like — just wanted to check first whether the current gating is deliberate, since I might be missing a reason it's cloud-only (quota or verification constraints on the OAuth client, maybe?). Entirely possible there's context I don't have.
Additional context
Worth flagging separately, since anyone who does get the tab enabled will hit it: the GSC error redirects don't land anywhere useful.
callback.tsredirects failures to${BASE_URL}/error, but the client app has no/errorroute, so the user gets a generic 404 with the message dropped. The "no properties found" branch uses${CLIENT_URL}, which isn't defined in either compose file or.env.example, producing aLocationofundefined/error?.... Your ownFEATURE_AUDIT.md:561documents this. The success path is fine.Relatedly, both
connect.ts:33andcallback.ts:63fall back to${process.env.SERVER_URL}/api/gsc/callbackwhenGOOGLE_REDIRECT_URIis unset, butSERVER_URLdoesn't seem to be defined anywhere in the repo — so the fallback yields a literalundefined/api/gsc/callback. SettingGOOGLE_REDIRECT_URIexplicitly works around it. Happy to split these into their own issue if you'd rather keep this one focused.