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

> Learn how to configure Google Search Console integration in OpenSEO with our self-hosted OAuth setup guide. Access performance analytics and URL inspection data securely.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: how-to-guide
- Published: 2026-08-07

---

**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`](https://github.com/every-app/open-seo/blob/main/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:

```env
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`](https://github.com/every-app/open-seo/blob/main/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:

- **Environment Layer** ([`src/server/features/gsc/oauth-config.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/oauth-config.ts)): Reads and validates `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`
- **OAuth Flow Layer** ([`src/server/features/gsc/selfHostedOAuth.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/selfHostedOAuth.ts)): Generates consent URLs, validates state tokens, and exchanges authorization codes
- **Grant Storage** ([`src/db/schema.ts`](https://github.com/every-app/open-seo/blob/main/src/db/schema.ts)): Persists encrypted refresh tokens in the `account` table via `GscConnectionRepository`
- **Service Layer** ([`src/server/features/gsc/services/GscService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)): Provides the canonical entry point for all GSC operations including `getPerformance()` and `inspectUrls()`
- **Server Functions** ([`src/serverFunctions/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts)): Exposes typed endpoints like `startSelfHostedGscLink`, `listGscSites`, and `setGscSite`
- **Client Helpers** ([`src/client/features/gsc/startGscLink.ts`](https://github.com/every-app/open-seo/blob/main/src/client/features/gsc/startGscLink.ts)): UI-facing utilities that launch the OAuth flow

### 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:

```env
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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts):

```typescript
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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/selfHostedOAuth.ts) validates the state and exchanges the code:

```typescript
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.

### 4. Select and Link a GSC Property

Once authenticated, call `listGscSites` to retrieve verified properties:

```typescript
// 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:

```typescript
// 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

```typescript
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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts), which internally calls `client.querySearchAnalytics()`.

### URL Inspection

For individual URL analysis, use the `inspectUrls` endpoint:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.