# How OpenSEO Handles Google Analytics 4 Data Fetching: A Deep Dive into the Architecture

> Discover how OpenSEO fetches Google Analytics 4 data using a layered TypeScript architecture. Learn about OAuth, property discovery, and report execution for seamless GA4 integration.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: deep-dive
- Published: 2026-08-14

---

**OpenSEO uses a layered TypeScript architecture with separate services for OAuth connection management, property discovery via the GA4 Admin API, and report execution via the GA4 Data API.**

The open-source SEO platform OpenSEO integrates Google Analytics 4 through a clean separation of concerns. This article examines how the codebase in `every-app/open-seo` authenticates users, manages connections, and fetches analytics data—complete with actual file paths and implementation details from the source.

## GA4 Authentication: OAuth Grants and Connection State

OpenSEO stores GA4 authentication in a shared OAuth table. In [`src/shared/ga4.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/ga4.ts), Google Analytics is identified by `providerId = "google-analytics"`. The system supports two deployment modes:

- **Self-hosted deployments**: Use a custom OAuth app configured via `startSelfHostedGa4Link`
- **Hosted deployments**: Currently rely on `GA4_OAUTH_APP_PENDING` while awaiting Google approval

The server-function endpoints in [`src/serverFunctions/ga4.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/ga4.ts) expose five key operations:

- `getGa4Connection` — retrieve current project connection
- `listGa4Properties` — discover available GA4 properties
- `setGa4Property` — link a specific property to the project
- `disconnect` — remove the connection and clean up grants
- `startSelfHostedGa4Link` — initiate OAuth flow for self-hosted instances

These functions wrap `Ga4Service` methods and include telemetry via `captureServerEvent` for PostHog logging.

## Connection Management with Ga4Service

The `Ga4Service` class in [`src/server/features/ga4/services/Ga4Service.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/ga4/services/Ga4Service.ts) handles all connection-level business logic:

**`getConnection`** — Fetches the project-level GA4 connection from `Ga4ConnectionRepository`

**`userHasGrant`** — Validates whether the authenticated user has a stored Google OAuth grant

**`listPropertiesForUserWithGrantStatus`** — Iterates through all stored Google accounts, creates a GA4 Admin client via `createGa4AdminClient`, and retrieves GA4 properties for each grant. Properties requiring reconnection are flagged with `requiresReconnect`

**`setProperty`** — Validates the selected property against user grants, fetches property metadata via the Admin API, and upserts a `Ga4Connection` row

**`disconnect`** — Removes the project-level connection and deletes the user's grant if no other projects reference that GA4 account

```typescript
// List available GA4 properties for the current user
import { listGa4Properties } from "@/serverFunctions/ga4";

const { accounts } = await listGa4Properties({ 
  projectId: "proj_123" 
});

// Result includes account hierarchies with properties
// accounts: [{ accountId, properties: [{ propertyId, isSelected, ... }] }]

```

```typescript
// Connect a specific property to your project
import { setGa4Property } from "@/serverFunctions/ga4";

await setGa4Property({
  projectId: "proj_123",
  accountId: "1234567890",
  propertyId: "properties/9876543210"
});

```

## Report Execution: Ga4ReportingService

Data fetching happens in [`src/server/features/ga4/services/Ga4ReportingService.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/ga4/services/Ga4ReportingService.ts). The `runReport` method implements the core GA4 Data API interaction:

1. Creates a **GA4 Data client** using `createGa4DataClient` with the user's OAuth token
2. Builds a request with property ID, date range, metrics, and dimensions
3. Calls `client.runReport` from Google's Analytics Data API
4. Normalizes the response and maps errors to the `Ga4ReportError` union

Error handling distinguishes between `ga4_quota_exhausted`, `ga4_upstream_unavailable`, and other failure modes defined in [`src/server/lib/ga4Errors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/ga4Errors.ts).

Specialized services consume `Ga4ReportingService` for specific analytics needs:

- `Ga4MeasurementHealthService` — validates GA4 implementation health
- `Ga4OrganicOverviewService` — retrieves organic traffic summaries
- `SearchOpportunityService` — analyzes keyword-level opportunities

```typescript
// Fetch organic overview metrics
import { Ga4OrganicOverviewService } from "@/server/features/ga4/services/Ga4OrganicOverviewService";

const report = await Ga4OrganicOverviewService.run({
  propertyId: "properties/9876543210",
  dateRange: { start: "2024-01-01", end: "2024-01-31" },
  metrics: ["sessions", "users"]
});

```

## Client Library Abstraction

[`src/server/lib/ga4Client.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/ga4Client.ts) encapsulates Google API client creation:

- `createGa4AdminClient` — Admin API client for property discovery and metadata
- `createGa4DataClient` — Data API client for report execution

Both factories wrap API errors in custom error classes (`Ga4AdminApiError`, `Ga4TokenError`) for consistent handling upstream.

## Error Handling and Telemetry

All GA4 operations integrate with OpenSEO's observability layer. Server functions log events to PostHog via `captureServerEvent`, tracking actions like `ga4:property_select` and `ga4:disconnect`.

The `Ga4ReportError` union in [`src/server/lib/ga4Errors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/ga4Errors.ts) provides structured error types that UI components translate into user-facing messages: `ga4_not_connected`, `ga4_quota_exhausted`, `ga4_reconnect_required`, and others.

## Complete Data Flow

```

UI Component
    ↓
startSelfHostedGa4Link / listGa4Properties / setGa4Property (server functions)
    ↓
Ga4Service (connection logic)
    ↓
ga4Client.ts (Admin/Data client factories) ←→ Google OAuth / GA4 APIs
    ↓
Ga4ConnectionRepository (persistence)
    ↓
Ga4ReportingService.runReport (data fetching)
    ↓
Ga4OrganicOverviewService / Ga4MeasurementHealthService / SearchOpportunityService
    ↓
UI with normalized analytics data

```

## Summary

- **OAuth layer**: Shared grant storage with `providerId = "google-analytics"` for authentication state
- **Connection service**: `Ga4Service` in [`src/server/features/ga4/services/Ga4Service.ts`](https://github.com/every-app/open-seo/blob/main/src/server/features/ga4/services/Ga4Service.ts) manages property discovery and linking
- **Client abstraction**: [`ga4Client.ts`](https://github.com/every-app/open-seo/blob/main/ga4Client.ts) provides typed Admin and Data API clients
- **Reporting core**: `Ga4ReportingService.runReport` executes all GA4 Data API calls with normalized error handling
- **Domain services**: Specialized services consume the reporting layer for measurement health, organic overviews, and search opportunities
- **Observability**: Full PostHog telemetry and structured error unions for reliable operations

## Frequently Asked Questions

### How does OpenSEO authenticate with Google Analytics 4?

OpenSEO uses OAuth 2.0 with grants stored in a shared `account` table identified by `providerId = "google-analytics"`. Self-hosted deployments configure custom OAuth apps via `startSelfHostedGa4Link` in [`src/serverFunctions/ga4.ts`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/ga4.ts), while hosted deployments use a pending Google-approved application.

### What is the difference between Ga4Service and Ga4ReportingService?

`Ga4Service` handles connection lifecycle: discovering properties, validating grants, and persisting selected properties. `Ga4ReportingService` focuses exclusively on executing GA4 Data API reports and normalizing responses. This separation allows connection logic to evolve independently from data fetching implementations.

### Where does OpenSEO handle GA4 API rate limits and errors?

Error normalization occurs in two places. [`src/server/lib/ga4Errors.ts`](https://github.com/every-app/open-seo/blob/main/src/server/lib/ga4Errors.ts) defines the `Ga4ReportError` union with variants like `ga4_quota_exhausted`. The `Ga4ReportingService.runReport` method catches Google API errors and maps them to these structured types, enabling consistent UI messaging and retry logic across the application.

### Can OpenSEO fetch any GA4 metric or dimension?

Yes, through direct use of `Ga4ReportingService.runReport`. However, the specialized services (`Ga4OrganicOverviewService`, `Ga4MeasurementHealthService`) provide pre-configured metric sets for common SEO workflows. These services build upon the same underlying report execution layer with validated, SEO-focused metric selections.