# How OpenSEO Integrates with Google Search Console for Keyword Data: A Complete Technical Guide

> Learn how OpenSEO integrates with Google Search Console for keyword data. Discover its technical architecture for pulling metrics, impressions, and CTRs. Read the complete guide.

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

---

**OpenSEO pulls keyword metrics, impressions, and click-through rates from Google Search Console through a layered TypeScript architecture that manages OAuth grants in the `account` table, persists site connections via `GscConnectionRepository`, and queries the GSC REST API through the `GscService` facade.**

OpenSEO enables users to analyze search performance without leaving the application by integrating directly with the Google Search Console API. The codebase implements a clean separation between authentication, data persistence, and API communication, ensuring type-safe access to keyword metrics like clicks, impressions, and average position. This integration centers around the [`GscService`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) class, which orchestrates OAuth grants defined in [[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts) and delegates HTTP operations to a specialized client layer.

## Architecture of the GSC Integration

The integration follows a facade pattern that shields the frontend from Google API complexities. The architecture consists of three distinct layers: OAuth credential management, connection persistence, and low-level API communication.

### OAuth Grant Management

Authentication begins with the `GSC_OAUTH_PROVIDER_ID` constant defined in [[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts). When a user connects their Google account, OpenSEO stores the OAuth refresh token in the `account` table with `providerId` set to this GSC-specific identifier. The [`GscService.userHasGrant()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) method checks for the existence of this grant before attempting any API calls, ensuring the application never requests data without valid credentials.

### Connection Persistence

Site-level connections are managed by [[`GscConnectionRepository.ts`](https://github.com/every-app/open-seo/blob/main/GscConnectionRepository.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/repositories/GscConnectionRepository.ts), which maintains the relationship between an OpenSEO project and a specific GSC property (site URL). This repository handles the `siteUrl`, Google account email, and verification status, allowing projects to maintain persistent links to search properties even when users navigate away from the settings page.

### API Client Abstraction

The [[`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts) file provides a thin wrapper around the Google Search Console REST endpoints. It handles automatic token refresh, request construction for the `searchAnalytics` query method, and URL inspection calls. Errors are categorized into [`GscApiError`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts) and [`GscTokenError`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts) classes defined in [[`src/server/lib/gscErrors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts), enabling the service layer to distinguish between permission denials and authentication failures.

## Core Implementation Files

The following files comprise the complete integration stack:

- **[[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)** – Defines the `GSC_OAUTH_PROVIDER_ID` and provider-specific constants used during OAuth flows.
- **[[`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts)** – Low-level HTTP client that instantiates per-grant clients and exposes `listSites()`, `querySearchAnalytics()`, and `inspectUrl()` methods.
- **[[`src/server/lib/gscErrors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts)** – Custom error classes including `GscTokenError` for revoked credentials and `GscNotConnectedError` for missing project connections.
- **[[`src/server/features/gsc/repositories/GscConnectionRepository.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/repositories/GscConnectionRepository.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/repositories/GscConnectionRepository.ts)** – Drizzle ORM repository managing the `gsc_connections` table for project-to-property mappings.
- **[[`src/server/features/gsc/services/GscService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)** – Public API surface used by UI components and server functions; implements `setSite()`, `getPerformance()`, and `inspectUrls()`.
- **[[`src/server/mcp/tools/search-console-tools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/search-console-tools.ts)](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/search-console-tools.ts)** – TanStack server functions that expose `GscService` methods to the frontend via the Managed Cloud Platform (MCP) tooling layer.

## Step-by-Step Data Flow

Understanding the request lifecycle helps debug connection issues and optimize data fetching strategies.

### Authorizing Access

When a user initiates a connection, the application redirects to Google's OAuth endpoint using the provider ID from [[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts). Upon callback, the refresh token is encrypted and stored in the `account` table. The [`GscService`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) retrieves this token via internal user ID lookups whenever it instantiates a new GSC client.

### Listing Search Console Properties

To display available properties, [`GscService.listSitesForUserWithGrantStatus()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) creates a client instance using the stored refresh token and calls the underlying `listSites()` method. This returns every site the user can access, including permission levels such as `"siteUnverifiedUser"` or `"siteOwner"`.

### Establishing Site Connections

Selecting a property triggers [`GscService.setSite()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts), which validates that the chosen URL belongs to the selected Google account and is verified. If the site is unverified, the service throws a `FORBIDDEN` error before persisting the connection via [`GscConnectionRepository`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/repositories/GscConnectionRepository.ts).

### Fetching Keyword Analytics

Performance queries flow through [`GscService.getPerformance()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts), which retrieves the stored connection, builds a `searchAnalytics` request using `buildSearchAnalyticsRequest`, and executes `client.querySearchAnalytics()`. The response contains rows of keyword data with dimensions like `query`, `page`, or `country`, alongside metrics including `clicks`, `impressions`, `ctr`, and `position`.

### Inspecting URL Index Status

For debugging indexing issues, [`GscService.inspectUrls()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) accepts an array of URLs and sequentially calls `client.inspectUrl()` for each entry. This batch processing captures per-URL errors individually, preventing a single failed inspection from aborting the entire request.

## Working with the GscService API

The following TypeScript examples demonstrate common operations when interacting with the Google Search Console integration.

### Checking if a User Has a GSC Grant

Before displaying connection UI elements, verify the user has authenticated with Google:

```typescript
import { GscService } from "@/server/features/gsc/services/GscService";

const hasGrant = await GscService.userHasGrant(currentUser.id);
if (!hasGrant) {
  // Prompt user to connect Google Search Console
  console.log("No GSC authorization found");
}

```

*Source:* [`GscService.userHasGrant`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

### Retrieving Available Sites

List all properties accessible to the authenticated user:

```typescript
const siteList = await GscService.listSitesForUserWithGrantStatus(currentUser.id);

for (const account of siteList.accounts) {
  console.log(`Account: ${account.email}`);
  for (const site of account.sites) {
    console.log(`  - ${site.siteUrl} (${site.permissionLevel})`);
  }
}

```

*Source:* [`GscService.listSitesForUserWithGrantStatus`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

### Connecting a Project to a Property

Persist the association between an OpenSEO project and a specific GSC site:

```typescript
await GscService.setSite({
  projectId: "proj_123",
  organizationId: "org_456",
  siteUrl: "https://example.com/",
  accountId: "google-sub-789",
  userId: currentUser.id,
});

```

*Source:* [`GscService.setSite`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

### Querying Keyword Performance Data

Fetch clicks and impressions for specific keywords within a date range:

```typescript
const performance = await GscService.getPerformance({
  projectId: "proj_123",
  startDate: "2024-01-01",
  endDate: "2024-01-31",
  dimensions: ["query"],
  rowLimit: 250,
});

performance.rows.forEach(row => {
  console.log(`${row.keys[0]}: ${row.clicks} clicks, ${row.impressions} impressions`);
});

```

*Source:* [`GscService.getPerformance`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

### Inspecting Multiple URLs

Check the index status of specific pages:

```typescript
const inspection = await GscService.inspectUrls({
  projectId: "proj_123",
  urls: [
    "https://example.com/blog/post-1",
    "https://example.com/products/item-2",
  ],
  languageCode: "en",
});

inspection.results.forEach(result => {
  if (result.error) {
    console.error(`Failed to inspect ${result.url}: ${result.error}`);
  } else {
    console.log(`${result.url}: ${result.result?.indexStatus}`);
  }
});

```

*Source:* [`GscService.inspectUrls`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

### Handling Authentication Failures

Implement reconnection prompts when tokens expire or are revoked:

```typescript
try {
  const data = await GscService.getPerformance({ projectId, startDate, endDate });
} catch (error) {
  if (GscService.isExpectedGrantFailure(error)) {
    // Trigger UI reconnection flow
    showReconnectModal();
  } else {
    // Handle unexpected API errors
    console.error("Unexpected GSC error:", error);
  }
}

```

*Source:* [`GscService.isExpectedGrantFailure`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)

## Summary

- **OpenSEO uses `GSC_OAUTH_PROVIDER_ID`** defined in [[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts) to identify and store Google Search Console OAuth grants in the `account` table.
- **The `GscConnectionRepository`** persists project-to-site mappings, enabling persistent connections between OpenSEO projects and specific GSC properties.
- **`GscService`** provides a unified facade for listing sites, establishing connections, fetching `searchAnalytics` data, and inspecting URLs.
- **Low-level API operations** are handled by [[`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts), which manages token refresh and request construction.
- **Error classification** through [[`src/server/lib/gscErrors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts) allows the frontend to distinguish between recoverable authentication issues and permission denials.
- **Server function exposure** occurs via [[`src/server/mcp/tools/search-console-tools.ts`](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/search-console-tools.ts)](https://github.com/every-app/open-seo/blob/main/src/server/mcp/tools/search-console-tools.ts), bridging the service layer to the UI.

## Frequently Asked Questions

### How does OpenSEO store Google Search Console authentication tokens?

OpenSEO stores GSC authentication as OAuth grants in the `account` table using the provider identifier `GSC_OAUTH_PROVIDER_ID` from [[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts). The refresh token is encrypted and associated with the user's account ID, allowing the [[`gscClient.ts`](https://github.com/every-app/open-seo/blob/main/gscClient.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts) to instantiate authenticated sessions without requiring repeated logins.

### What happens when a user's GSC token expires or is revoked?

When the Google API returns a 401 or 403 error, the [`GscTokenError`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts) class captures the failure. The [`GscService.isExpectedGrantFailure()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) method identifies these specific errors, triggering the UI to display a reconnection prompt rather than crashing the application.

### Can OpenSEO connect to multiple Google Search Console properties simultaneously?

While a single OpenSEO project connects to one GSC property at a time via [`GscConnectionRepository`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/repositories/GscConnectionRepository.ts), the [`listSitesForUserWithGrantStatus()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) method allows users to view all accessible properties across multiple Google accounts. Users can switch connections by calling [`setSite()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) with a different `siteUrl` and `accountId`.

### How does the integration handle API rate limits and errors?

Error handling is centralized in [[`src/server/lib/gscErrors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts)](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscErrors.ts), which distinguishes between `GscApiError` (general API failures) and `GscTokenError` (authentication issues). The [`inspectUrls()`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts) method processes URLs sequentially with individual error catching, ensuring that temporary rate limiting on one URL does not prevent inspection of others in the batch.