Google Ads — Current Implementation
Authentication & Token Storage
OAuth 2.0 flow. Credentials stored on ConnectedChannel:
| Field | Content |
|---|---|
tokenInfo (encrypted) | { accessToken, refreshToken, expireOn } |
subChannelInfo (plain JSON) | { id: string } — Google Ads customer ID selected during OAuth |
The developer token (GOOGLE_ADS_DEVELOPER_TOKEN) is a platform-level secret required in every API request header.
Database Models
ConnectedChannel
Standard channel record. type = "google_ads". subChannelInfo.id is the Google Ads Customer ID.
CampaignExternalMapping
Links an internal Campaign record to a Google Ads campaign ID.
platform = "google_ads"
externalCampaignId = "123456789" // Google Ads campaign resource ID
connectedChannelId → ConnectedChannel
campaignId → CampaignCampaignMetrics
Daily/periodic performance snapshots synced by the insights worker.
SearchTermReport
Raw search queries pulled from search_term_view.
tenantId, campaignId, adGroupId?
searchTerm, matchType
impressions, clicks, cost, conversions, ctr, avgCpc
syncDate // date the data was fetched (UTC midnight)
@@unique([campaignId, searchTerm, syncDate])SearchTermClassification
AI + DM classification for each search term.
aiClassification: "add_as_keyword" | "add_as_negative" | "watch" | "irrelevant"
aiRationale: string
dmDecision: string | null // null until DM reviews
status: "pending" | "dm_reviewed" | "pushed" | "skipped"
pushedAt: DateTime | nullNegativeKeyword
Campaign-level negative keywords tracked after push.
source: "manual" | "search_term_classification"
status: "pending_push" | "active" | "removed"
externalCriterionId: string // Google Ads criterion resource ID after pushCampaignKeyword
Active positive keywords synced from Google Ads.
AdGroup
Ad groups with default bids.
GoogleAdsService (packages/providers/google/src/google-ads.ts)
Key methods:
| Method | What it does |
|---|---|
getAllCustomers() | Lists accessible top-level accounts (for sub-channel selection) |
getCampaignsAsLookup() | Returns { id, name } list for all campaigns |
getCampaignDailyMetrics(from, to) | Daily impressions/clicks/cost/conversions per campaign |
getSearchTerms(from, to, campaignId?) | Pulls search_term_view rows for the period |
getKeywords(campaignId?) | Lists active keywords with bid + quality score |
getAdGroups(campaignId?) | Lists ad groups |
getNegativeKeywords(campaignId?) | Lists campaign-level negatives |
addNegativeKeyword(campaignId, adGroupId, keyword, matchType) | Adds a negative keyword |
removeNegativeKeyword(campaignId, criterionId) | Removes a negative keyword |
getKeywordMetricsByKeyword(keyword) | Historical search volume for a keyword |
All calls use the Authorization: Bearer {accessToken} + developer-token + optional login-customer-id headers. Token refresh is not yet automated in the analytics endpoint — a stale token will return a 401.
Workers
google-ads-insights (agent__google-ads-insights)
- Triggered manually via the Insights tab on the channel detail page.
- Fetches campaign list + daily metrics for the selected period.
- Passes data to Claude (role:
google-ads-insights) to generate a narrative report. - Saves result as a
ChannelInsightrecord.
google-ads-writer (agent__google-ads-writer)
- Generates RSA (Responsive Search Ad) headline/description copy.
- Standard content worker pipeline — uses
creditType: "google_ads_rsa".
search-term-classifier (agent__search-term-classifier)
- Receives a batch of
SearchTermReportIDs +campaignId. - Fetches term metrics from DB.
- Calls Claude with
AgentConfigfor rolesearch-term-classifier. - Upserts
SearchTermClassificationrecords withstatus: "pending". - Concurrency: 2 workers.
Analytics Endpoint
GET /tenant/v1/channels/:id/analytics?from=YYYY-MM-DD&to=YYYY-MM-DD
Supported channel types: google_ads, google_search_console, google_analytics, google_business_profile, facebook, instagram, linkedin.
Response shape for google_ads:
{
"period": { "from": "...", "to": "..." },
"stats": {
"impressions": { "current": 0, "previous": 0, "deltaPercent": 0 },
"clicks": { "current": 0, "previous": 0, "deltaPercent": 0 },
"ctr": { "current": 0, "previous": 0, "deltaPercent": 0 },
"avgCpc": { "current": 0, "previous": 0, "deltaPercent": 0 },
"cost": { "current": 0, "previous": 0, "deltaPercent": 0 },
"conversions": { "current": 0, "previous": 0, "deltaPercent": 0 }
},
"campaigns": [
{
"id": "", "name": "", "status": "ENABLED",
"impressions": 0, "clicks": 0, "ctr": 0,
"avgCpc": 0, "cost": 0, "conversions": 0
}
],
"dailyPerformance": [
{ "date": "YYYY-MM-DD", "impressions": 0, "clicks": 0, "cost": 0 }
]
}Dashboard UI
apps/dashboard/src/app/(dashboard)/channels/[id]/GoogleAdsChannelDetail.tsx
Three inner tabs under Analytics:
- Overview — 6 stat cards with delta vs previous period + daily impressions/clicks line chart
- Campaigns — sortable/filterable table (name, status, impressions, clicks, CTR, avg CPC, cost, conversions)
- Daily — impressions/clicks line chart + cost per day bar chart
Plus an Insights tab shared via ChannelInsightsTab.