How to Configure Google Search Console Integration in OpenSEO: Self-Hosted OAuth Setup Guide

OpenSEO connects to Google Search Console through an OAuth 2.0 flow that stores encrypted refresh tokens and maps verified properties to projects, enabling read-only access to performance analytics and URL inspection data.

Configuring Google Search Console integration allows OpenSEO to pull search performance metrics directly into your dashboard. Whether you are running the hosted SaaS version or a self-hosted instance, the platform uses a secure OAuth-based connector to authenticate with Google APIs and maintain persistent access to your verified properties.

Prerequisites and Environment Configuration

Before initiating the connection, self-hosted deployments must define three critical environment variables. According to the source code in src/server/features/gsc/oauth-config.ts, the application validates these credentials through hasSelfHostedGscConfig() before exposing the "Connect GSC" functionality.

Add the following to your .env file or container environment:

GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
BETTER_AUTH_SECRET=32-plus-character-random-string

The BETTER_AUTH_SECRET must be at least 32 characters and is used to sign the OAuth state parameter for CSRF protection. After updating these values, restart your OpenSEO instance—the self-hosted pre-flight check in src/lib/selfhost-preflight.ts will warn if any variable is missing.

Understanding the OAuth Architecture

The integration follows a layered architecture that separates credential management, token exchange, and business logic. As implemented in every-app/open-seo, the flow spans multiple modules:

Self-Hosted vs. Hosted Mode

Hosted (SaaS) deployments use OpenSEO's pre-configured Better-Auth instance, so the client simply calls authClient.oauth2.link without additional environment setup.

Self-Hosted deployments require you to supply the Google OAuth credentials and BETTER_AUTH_SECRET. The server validates these via hasSelfHostedGscConfig() before enabling the connection button.

Step-by-Step Configuration Guide

1. Configure Environment Variables

Ensure your deployment includes the three required variables. For Docker deployments, pass these as environment variables to your container:

GOOGLE_CLIENT_ID=AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BETTER_AUTH_SECRET=super-long-random-string-at-least-32-chars

2. Initiate the OAuth Flow

When a user clicks Connect Google Search Console, the client invokes startGscLink(callbackURL). In self-hosted mode, this triggers startSelfHostedGscLink from src/serverFunctions/gsc.ts:

import { startGscLink } from '@/client/features/gsc/startGscLink';

async function onConnectClick() {
  const callbackUrl = `${window.location.origin}/gsc/oauth-callback`;
  await startGscLink(callbackUrl);
}

Internally, the server function retrieves the OAuth configuration and generates a signed state token:

// src/serverFunctions/gsc.ts
export const startSelfHostedGscLink = createServerFn({ method: 'POST' })
  .middleware(requireAuthenticatedContext)
  .validator(startSelfHostedLinkSchema)
  .handler(async ({ data, context }) => {
    const publicOrigin = getPublicOrigin(getRequest());
    const url = await createSelfHostedGscAuthorizationUrl({
      user: { userId: context.userId, userEmail: context.userEmail },
      callbackURL: data.callbackURL,
      publicOrigin,
    });
    return { url };
  });

The browser redirects to Google's consent screen requesting the https://www.googleapis.com/auth/webmasters scope.

3. Handle the OAuth Callback

After user authorization, Google redirects to your callbackURL. The handler in src/server/features/gsc/selfHostedOAuth.ts validates the state and exchanges the code:

export async function handleSelfHostedGscOAuthCallback({ request, user, publicOrigin }) {
  const config = await getGscOAuthClientConfig();
  if (!config) return new Response('Missing Google Search Console OAuth configuration', { status: 500 });

  const url = new URL(request.url);
  const stateParam = url.searchParams.get('state');
  const state = await verifyState(stateParam!, config.clientSecret);
  if (state.userId !== user.userId) return new Response('Search Console OAuth user mismatch', { status: 403 });

  const code = url.searchParams.get('code');
  const tokens = await exchangeCode({ 
    code, 
    clientId: config.clientId, 
    clientSecret: config.clientSecret, 
    redirectUri: getRedirectUri(publicOrigin) 
  });
  
  await upsertGrant({ user, tokens });
  return new Response(null, { status: 303, headers: { Location: state.callbackPath } });
}

The upsertGrant function persists the encrypted refresh token to the account table in your database.

Once authenticated, call listGscSites to retrieve verified properties:

// Server function usage
const sites = await listGscSites();
// Returns sites via GscService.listSitesForUserWithGrantStatus()

After the user selects a property, invoke setGscSite to link it to the project:

// src/serverFunctions/gsc.ts
export const setGscSite = createServerFn({ method: 'POST' })
  .middleware(requireProjectContext)
  .validator(setSiteSchema)
  .handler(async ({ data, context }) => {
    const connection = await GscService.setSite({
      projectId: context.projectId,
      organizationId: context.organizationId,
      accountId: data.accountId,
      siteUrl: data.siteUrl,
      userId: context.userId,
    });
    return { connected: true as const, siteUrl: connection.siteUrl };
  });

The GscService.setSite() method validates that the property is verified in Google Search Console before storing the mapping in GscConnectionRepository.

Accessing Performance Data and URL Inspection

With the connection established, you can query search analytics through GscService methods. All calls are read-only and do not consume external API credits.

Querying Performance Metrics

import { GscService } from '@/server/features/gsc/services/GscService';

const perf = await GscService.getPerformance({
  projectId: 'proj_123',
  startDate: '2024-07-01',
  endDate: '2024-07-31',
  dimensions: ['query'],
  rowLimit: 100,
});
console.log(perf.rows); // Array of Search Console rows

This corresponds to the getPerformance server function in src/serverFunctions/gsc.ts, which internally calls client.querySearchAnalytics().

URL Inspection

For individual URL analysis, use the inspectUrls endpoint:

const inspection = await GscService.inspectUrls({
  projectId: 'proj_123',
  urls: ['https://example.com/page'],
});

This invokes the Google Search Console API's client.inspectUrl method through the service layer.

Summary

  • Environment Setup: Self-hosted instances require GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BETTER_AUTH_SECRET defined in the environment.
  • OAuth Flow: The integration uses createSelfHostedGscAuthorizationUrl() to generate consent URLs and handleSelfHostedGscOAuthCallback() to process Google's redirect.
  • Token Storage: Encrypted refresh tokens are stored in the account table via upsertGrant(), with property mappings maintained in GscConnectionRepository.
  • Service Layer: GscService provides the canonical interface for listing sites, setting properties, and fetching performance data.
  • Read-Only Access: All integrations use the webmasters scope for read-only access to search analytics and URL inspection data.

Frequently Asked Questions

What environment variables are required for Google Search Console integration in OpenSEO?

Self-hosted deployments must define GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BETTER_AUTH_SECRET. The system validates these through hasSelfHostedGscConfig() in src/server/features/gsc/oauth-config.ts before enabling the connection feature. Hosted SaaS users do not need to configure these variables manually.

How does OpenSEO store Google Search Console authentication tokens?

The platform stores encrypted refresh tokens in the account table using GscConnectionRepository, as defined in src/db/schema.ts. When a user completes the OAuth flow, handleSelfHostedGscOAuthCallback() calls upsertGrant() to persist the tokens, enabling background data synchronization without requiring re-authentication.

Can I connect multiple Google Search Console properties to one OpenSEO project?

Currently, the setGscSite function in src/serverFunctions/gsc.ts links a single GSC property to a project via GscService.setSite(). This validates the property's verification status before storing the mapping. To switch properties, you must call setGscSite again with a different siteUrl and accountId.

Is the Google Search Console integration read-only?

Yes. The OAuth scope https://www.googleapis.com/auth/webmasters grants read-only access. The GscService methods getPerformance() and inspectUrls() only query data through client.querySearchAnalytics() and client.inspectUrl() respectively, without modifying your Search Console configuration or consuming external API credits.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →