# How OpenSEO Integrates with Google Search Console for Performance Data

> Discover how OpenSEO integrates with Google Search Console using OAuth and a REST client to access crucial performance metrics. Enhance your SEO strategy today.

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

---

**OpenSEO integrates with Google Search Console through a layered architecture involving OAuth authentication via Better-Auth, a thin REST client wrapper around the Google Webmasters v3 API, and server-side service functions that expose performance metrics to the frontend.**

OpenSEO provides **first-party, free access** to Google Search Console (GSC) performance data without requiring third-party API credits. This deep integration allows users to pull organic search metrics—clicks, impressions, CTR, and position—directly into their SEO dashboards. Below is a complete technical breakdown of how every-app/open-seo implements this flow, from initial OAuth grant to final data consumption.

## OAuth Authentication and Account Linking

The integration begins when users grant OpenSEO **read-only access** to their Search Console properties. This flow uses Better-Auth for secure OAuth handling.

The provider constant lives in **[`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts)**, which defines:

```typescript
export const GOOGLE_SEARCH_CONSOLE_PROVIDER_ID = "google-search-console";

```

When a user initiates connection, the server function `startSelfHostedGscLink` (in [`src/serverFunctions/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts)) builds an authorization URL using the self-hosted flow. Upon successful authorization, Better-Auth stores the grant in the `account` table with the provider ID `google-search-console`.

## Token Management and GSC REST Client

Every API call requires a **fresh short-lived access token**. The `gscClient` handles this automatically.

In **[`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts)**, the client calls:

```typescript
const token = await getAuth().api.getAccessToken({
  accountId: account.id,
  providerId: GOOGLE_SEARCH_CONSOLE_PROVIDER_ID,
});

```

Token failures—expired or revoked grants—are wrapped in `GscTokenError`. This allows upstream code to distinguish **expected grant failures** from unexpected errors.

The `createGscClient` factory returns a thin wrapper around `https://www.googleapis.com/webmasters/v3` with three core methods:

- **`listSites()`** – Enumerates all verified properties the grant can access
- **`querySearchAnalytics()`** – Executes `searchAnalytics.query` for performance data
- **`inspectUrl()`** – Calls the URL Inspection API

## Service Layer: GscService

**`GscService`** ([`src/server/features/gsc/services/GscService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/gsc/services/GscService.ts)) consolidates all GSC business logic. It exposes five high-level operations:

| Method | Purpose |
|--------|---------|
| `userHasGrant()` | Checks if a valid OAuth grant exists for the user |
| `listSitesForUserWithGrantStatus()` | Lists sites with grant status for each |
| `setSite()` | Stores the selected property for a project |
| `getPerformance()` | Fetches search analytics data with filtering |
| `inspectUrls()` | Runs URL Inspection API calls |

The service also implements **error normalization**. When `GscTokenError` bubbles up, `isExpectedGrantFailure()` returns `true`, signaling the UI to prompt reconnection without surfacing raw stack traces.

## Server Functions for Frontend Consumption

OpenSEO exposes GSC functionality through **TanStack server functions** that React components consume via TanStack Query.

### Connection Management ([`src/serverFunctions/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts))

```typescript
// List all accessible GSC accounts and sites
const { accounts } = await listGscSites({ projectId: "proj_123" });

// Bind a selected property to the project
await setGscSite({ 
  projectId: "proj_123", 
  siteUrl: "https://example.com",
  accountId: "acc_456" 
});

```

### Performance Reporting ([`src/serverFunctions/searchPerformance.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/searchPerformance.ts))

The `getSearchPerformanceReport` function orchestrates **multiple GSC queries** to build a complete dashboard dataset:

```typescript
import { getSearchPerformanceReport } from "@/serverFunctions/searchPerformance";

const report = await getSearchPerformanceReport({
  projectId: "proj_123",
  dateRange: { preset: "last_28_days" },
  device: "mobile",
  country: "USA",
});

if (report.connected) {
  console.log(report.totals);           // Aggregated clicks, impressions, CTR, position
  console.log(report.strikingDistance); // Query/page rows near ranking thresholds
  console.log(report.countries);        // Country breakdown
}

```

Behind this single call, `getSearchPerformanceReport` invokes `GscService.getPerformance` **three times**:

1. **Daily totals** for current and previous periods (trending)
2. **Query/page rows** filtered for "striking-distance" analysis (positions 4-20 with high impressions)
3. **Country breakdown** for geographic insights

## Direct Service Integration (Advanced Use)

For custom logic or testing, import `GscService` directly:

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

const performance = await GscService.getPerformance({
  projectId: "proj_123",
  startDate: "2024-01-01",
  endDate: "2024-01-31",
  dimensions: ["query", "page"],
  filters: [
    { dimension: "device", operator: "equals", expression: "MOBILE" }
  ],
  rowLimit: 500,
});

// Raw GSC API response shape
console.log(performance.rows); // Array of { keys: [...], clicks, impressions, ctr, position }

```

## Error Handling and Reconnection Flow

The integration gracefully handles **token lifecycle edge cases**:

- **Missing grant** → UI shows "Connect Google Search Console" card
- **Expired/revoked token** → `GscTokenError` triggers `isExpectedGrantFailure()`, prompting reconnection without error spam
- **API quota exceeded** → Standard HTTP error propagation with retry guidance

This design ensures users understand when **re-authorization** is needed versus when transient errors occur.

## Summary

- **OAuth flow** uses Better-Auth with provider ID defined in [`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts), storing grants in the `account` table
- **Token minting** happens automatically in [`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts) via `getAuth().api.getAccessToken`
- **REST client** wraps Google Webmasters v3 API with `listSites`, `querySearchAnalytics`, and `inspectUrl` methods
- **Service layer** (`GscService`) provides high-level operations: grant checks, site listing, property binding, and performance queries
- **Server functions** in [`src/serverFunctions/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts) and [`src/serverFunctions/searchPerformance.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/searchPerformance.ts) expose typed endpoints to the React frontend
- **Error handling** distinguishes expected grant failures from unexpected errors, guiding users toward reconnection

## Frequently Asked Questions

### What OAuth scopes does OpenSEO request from Google Search Console?

OpenSEO requests **read-only scopes** for Search Console data. The exact scope configuration is defined alongside the provider ID in [`src/shared/gsc.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gsc.ts). The integration never requests write access to properties or site settings.

### Can OpenSEO access multiple GSC properties for one project?

Yes. A user can have **multiple Google accounts** connected via Better-Auth grants. The `listGscSites` server function returns all accessible properties across all linked accounts, and `setGscSite` binds one property per project. Switching properties requires calling `setGscSite` again with a different `siteUrl`.

### How does OpenSEO handle GSC API rate limits?

The thin REST client in [`src/server/lib/gscClient.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/gscClient.ts) propagates standard HTTP errors from Google's API. The service layer does not implement custom retry logic—rate limit handling is currently delegated to calling code. For heavy usage, the `rowLimit` parameter in `GscService.getPerformance` allows pagination control to stay within quotas.

### Is the raw GSC data cached or stored in OpenSEO's database?

Per the source analysis, **performance data is not persisted** in OpenSEO's database. Each dashboard view triggers fresh GSC API calls through `GscService.getPerformance`. Only the **OAuth grant** and **selected property binding** are stored (in `account` and `GscConnectionRepository` respectively). This design ensures data freshness while minimizing storage requirements.