How to Connect Google Analytics 4 to OpenSEO: Complete Integration Guide
Connect Google Analytics 4 to OpenSEO using Better Auth OAuth in two modes—hosted SaaS or self-hosted with Google Cloud OAuth credentials—to enable read-only analytics reports.
OpenSEO integrates Google Analytics 4 through a dedicated Better Auth OAuth provider called google-analytics. The connection is read-only: encrypted OAuth tokens are stored in the Better Auth account table, and your selected property metadata is saved on the project record. This guide covers both hosted and self-hosted deployment modes using the actual source code implementation from the every-app/open-seo repository.
Prerequisites for Google Analytics 4 Connection
Before connecting GA4 to OpenSEO, ensure you have:
- A Google Cloud project with the Google Analytics Admin API (
analyticsadmin.googleapis.com) and Google Analytics Data API (analyticsdata.googleapis.com) enabled - For self-hosted deployments: a Google OAuth 2.0 client ID and secret
The required OAuth scope is https://www.googleapis.com/auth/analytics.readonly, defined in src/shared/ga4.ts alongside the provider constant GA4_OAUTH_PROVIDER_ID.
Hosted Mode vs. Self-Hosted Mode
Hosted Mode (SaaS)
In hosted mode, OpenSEO's SaaS infrastructure handles the entire OAuth flow. You only need to click "Connect with Google" in the UI—no environment configuration required.
Self-Hosted Mode
Self-hosted deployments require manual OAuth client setup:
-
Create a Google Cloud OAuth client with redirect URI:
https://<your-domain>/api/ga4/oauth/callback -
Set three environment variables:
GOOGLE_CLIENT_ID=your_client_id GOOGLE_CLIENT_SECRET=your_client_secret BETTER_AUTH_SECRET=your_random_32_char_secret -
Restart the service to load variables
See docs/SELF_HOSTING_GOOGLE_ANALYTICS.md for the complete step-by-step guide including API enablement and consent screen configuration.
Step-by-Step Connection Flow
1. Trigger the OAuth Flow in the UI
Navigate to Project → Settings → Integrations → Analytics and click "Connect with Google". This invokes startGoogleLink("ga4", …) from src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx:
// Inside GoogleAnalyticsConnectionCard.tsx
const handleConnect = () => void startGoogleLink("ga4", window.location.href);
<button
type="button"
onClick={handleConnect}
className="inline-flex items-center …"
>
<GoogleGlyph className="size-[18px]" />
Connect with Google
</button>
The startGoogleLink function builds the authorization URL for either hosted or self-hosted mode and redirects to Google's consent screen.
2. Handle the OAuth Callback
After user consent, Google redirects to /api/ga4/oauth/callback. In self-hosted mode, src/server/features/google/selfHostedOAuth.ts exchanges the code for tokens:
export async function createSelfHostedGoogleAuthorizationUrl({
providerId,
callbackURL,
}: { providerId: string; callbackURL: string }) {
const clientId = process.env.GOOGLE_CLIENT_ID!;
const scope = encodeURIComponent(
"openid email profile https://www.googleapis.com/auth/analytics.readonly"
);
const redirectUri = encodeURIComponent(callbackURL);
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&access_type=offline`;
}
The server handler stores tokens via Better Auth and redirects back to the UI.
3. Select Your GA4 Property
The UI fetches available properties through listGa4Properties in src/serverFunctions/ga4.ts:
export const listGa4Properties = createServerFn({ method: "POST" })
.middleware(requireProjectContext)
.validator(projectScopedSchema)
.handler(async ({ context }) => {
const [propertyList, connection] = await Promise.all([
Ga4Service.listPropertiesForUserWithGrantStatus(context.userId),
Ga4Service.getConnection(context.projectId),
]);
return {
accounts: propertyList.accounts.map(grant => ({
...grant,
properties: grant.properties.map(property => ({
...property,
isSelected:
connection?.ga4AccountId === grant.accountId &&
connection.propertyId === property.propertyId,
})),
})),
};
});
This server function aggregates the user's GA4 accounts, marks any currently selected property, and returns data for the picker UI.
4. Save the Property Selection
When you choose a property, setGa4Property persists the linkage:
export const setGa4Property = createServerFn({ method: "POST" })
.middleware(requireProjectContext)
.validator(setPropertySchema)
.handler(async ({ data, context }) => {
const connection = await Ga4Service.setProperty({
projectId: context.projectId,
organizationId: context.organizationId,
accountId: data.accountId,
propertyId: data.propertyId,
userId: context.userId,
});
waitUntil(
captureServerEvent({
distinctId: context.userId,
event: "ga4:property_select",
organizationId: context.organizationId,
properties: { project_id: context.projectId },
})
);
return {
connected: true as const,
propertyId: connection.propertyId,
propertyDisplayName: connection.propertyDisplayName,
};
});
Ga4Service.setProperty stores the selection, and the event is tracked for analytics.
Core Implementation Architecture
| Component | Role | Key File |
|---|---|---|
| OAuth provider & scopes | Defines Better Auth provider ID and required scopes | src/shared/ga4.ts |
| Connection UI | React component for status, property picker, disconnect | src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx |
| Server functions | API endpoints for connection lifecycle | src/serverFunctions/ga4.ts |
| GA4 service layer | Google API calls, caching, property storage | src/server/features/ga4/services/* |
| Self-hosted OAuth | URL builder and callback validation | src/server/features/google/selfHostedOAuth.ts |
Using Connected GA4 Data
Once connected, all analytics reports—dashboard cards, organic overview, traffic acquisition, and more—read from the GA4 Data API using Ga4Service with the stored OAuth token. Server functions in src/serverFunctions/ga4.ts wrap these calls for the UI.
If the connection expires or is revoked, the UI automatically falls back to the connection card, prompting re-authentication.
Summary
- OpenSEO uses Better Auth with a dedicated
google-analyticsOAuth provider for GA4 integration - Two modes: hosted (managed) or self-hosted (requires Google Cloud OAuth client)
- Required scope:
https://www.googleapis.com/auth/analytics.readonly(read-only access) - Key environment variables:
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,BETTER_AUTH_SECRET - Core files:
src/shared/ga4.ts,src/serverFunctions/ga4.ts,src/client/features/ga4/GoogleAnalyticsConnectionCard.tsx, andsrc/server/features/ga4/services/Ga4Service
Frequently Asked Questions
What happens to my GA4 data after connecting OpenSEO?
OpenSEO stores only encrypted OAuth tokens and property metadata (property ID, account ID, display name). No actual analytics data is retained long-term; reports are fetched live from Google's Data API on each request using your stored credentials.
Can I connect multiple GA4 properties to one OpenSEO project?
No. Each OpenSEO project links to one GA4 property at a time. You can change properties by disconnecting and reconnecting, or create separate OpenSEO projects for different properties.
Why does my self-hosted connection fail with "redirect_uri_mismatch"?
This error occurs when the redirect URI in your Google Cloud OAuth client doesn't match your actual deployment URL. Ensure you've added both your production URL (https://<your-domain>/api/ga4/oauth/callback) and any local development URLs (e.g., http://localhost:3000/api/ga4/oauth/callback) to the authorized redirect URIs in Google Cloud Console.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →