OAuth Property Connect + Audience Module Hardening

Three large changes shipped together: creators can now connect their actual social channels via OAuth (instead of self-reporting metrics), the Audiences module gets a real CSV upload + hashing pipeline, and the wall-clock activation theater is replaced with a real worker. Plus Sign-in-with-Google is fully wired and pulls credentials from the same master row as the YouTube Connect flow.

Three large changes shipped together: creators can now connect their actual social channels via OAuth (instead of self-reporting metrics), the Audiences module gets a real CSV upload + hashing pipeline, and the wall-clock activation theater is replaced with a real worker. Plus Sign-in-with-Google is fully wired and pulls credentials from the same master row as the YouTube Connect flow.

Why this matters

Self-reported follower counts and engagement rates are the biggest trust gap in any creator marketplace. With this release, properties verified via OAuth carry a green badge backed by data pulled directly from YouTube Data API / Meta Graph. Advertisers see real numbers; creators win deals on trust instead of inflated claims.

The Audiences module previously had a fake "wall-clock" activation pipeline that progressed PENDING → SYNCING → ACTIVE based on elapsed seconds, with zero actual platform pushes. Customers viewing this would have been misled about what the platform delivers. That's gone.

1. Property Connect (YouTube + Instagram + Facebook)

#### Master OAuth providers (admin-managed)

  • New master_oauth_providers table (migration d5e6f7a8b9c0) — one row per platform with admin-editable Client ID / Client Secret (encrypted) / App URL / Callback URL.
  • Admin → Master Settings → "OAuth Providers" tab — paste credentials, toggle enable, see masked values. Per-platform terminology (Google's "Client ID/Secret" vs Meta's "App ID/Secret").
  • Migration e6f7a8b9c0d1 adds app_url + redirect_uri columns. Migration a8b9c0d1e2f3 seeds Instagram + Facebook rows.

#### Connect flow (5 endpoints per platform)

| Method | Path | Purpose | |---|---|---| | POST | /properties/connect/{platform}/init | Returns OAuth consent URL + CSRF state token | | GET | /properties/connect/{platform}/callback | Public HTML page that postMessages auth code to opener | | POST | /properties/connect/{platform}/exchange | Code → long-lived token → list of accounts | | POST | /properties/connect/{platform}/finalize | Save chosen channel as verified property | | POST | /properties/connect/{property_id}/refresh-metrics | Re-pull live metrics | | POST | /properties/connect/{property_id}/disconnect | Revoke + downgrade to unverified |

#### Per-platform services

  • youtube_service — Google OAuth + YouTube Data API v3. Pulls subscribers, computes engagement from last 30 videos.
  • meta_service — Meta Login OAuth + long-lived (60-day) token swap.
  • instagram_serviceInstagram-with-Instagram-Login flow (api.instagram.com). Bypasses Facebook Page mediation — works without an FB Page linkage. Uses instagram_business_basic scope.
  • facebook_service — Facebook Pages connect via Meta Graph API.

Token storage: encrypted with AES-256-GCM (Fernet) in new property_oauth_credentials table. Cross-workspace uniqueness on (platform, external_account_id) prevents one channel being claimed by two workspaces. Migration b9c0d1e2f3a4 adds 'FACEBOOK' to the property_platform PG enum.

#### Frontend

  • Per-platform Connect buttons in the Add Property modal (ConnectYouTubeButton, ConnectInstagramButton, ConnectFacebookButton).
  • Channel/page picker modal for multi-account scenarios (e.g. one Google account managing multiple YouTube channels).
  • Field locking on OAuth-verified properties — handle, profile URL, follower count, engagement rate, avg views become read-only with green "synced" badge. Disconnect button to unlock.
  • Race-condition-fixed state machine: setStep('finalizing') fires synchronously when the OAuth message arrives, so the popup-close watcher doesn't false-fire "cancelled" while the exchange is in flight.

#### Sign-in-with-Google

  • auth.py now reads Google OAuth credentials from master_oauth_providers.youtube row first, falls back to env vars. Single source of truth — same Google project serves both YouTube Connect AND Sign-in-with-Google.
  • FRONTEND_URL configurable via env (was hardcoded to localhost:3005).
  • Fixed create_access_token() call signature mismatch in OAuth path.
  • Fixed family_id NULL violation when issuing refresh token on OAuth login.
  • Login page reads NEXT_PUBLIC_API_BASE_URL (was looking at the wrong env var).
  • Social login buttons (Google / LinkedIn / Facebook) on /login are now hidden behind a SHOW_SOCIAL_LOGIN flag (default false) until OAuth apps clear platform review.

2. Audience Module Hardening

#### CSV upload + hashing

  • New audience_identifiers table (migration c0d1e2f3a4b5) — SHA-256 hashed identifiers tied to either a Custom Audience or a Suppression List. Plaintext is never stored.
  • services/audience/identifier_hasher.py — CSV parser with auto-detect (email / phone / ad_id), normalization (lowercase email, digits-only phone) → SHA-256.
  • POST /audience/custom/{id}/upload and POST /audience/suppression/{id}/upload — multipart endpoints. 50 MB cap, dedupe via unique constraint, return summary {raw_rows, hashed, deduped, skipped}.
  • New CsvUploadModal component — drag-drop, format hint, sample.csv download, identifier-type override, results display.
  • Sample file at /public/sample-audience.csv for download from the modal.

#### Suppression query helper

  • services/audience/suppression_query.pyget_suppressed_hashes_for_campaign() returns hashed identifiers to exclude when targeting a campaign. Canonical hook for any future trafficking code.

#### Real activation worker (replaces wall-clock theater)

  • services/audience/platform_activator.py — per-platform pushers (Meta / Google Ads / TikTok) with DRY-RUN mode. Logs exactly what would be sent (count, sample of 3 truncated hashes, target endpoint), inserts an audience_activation_log row, marks status as ACTIVE.
  • New audience_activation_log table — audit trail of every push attempt (queued / started / dry_run / success / failed) with payload summary.
  • POST /audience/activate now enqueues a real BackgroundTask that drives SegmentActivation through PENDING → SYNCING → ACTIVE / FAILED.
  • GET /audience/segments/{id}/activations reflects real worker state — no more time-based simulation.
  • To switch to real Meta/Google/TikTok pushes when Marketing API creds are configured: set _DRY_RUN = False at the top of platform_activator.py and fill in 3 lines per platform.

#### SA visibility

  • All "Create" CTAs hidden from Super Admin login on the Audiences page (per CLAUDE.md tenant-scoping rule). SA sees a clean read-only view across all workspaces.
  • /analytics/custom-kpis GET now skips workspace filter for SA → returns all KPIs across workspaces.
  • Custom KPIs page gets a search box (filters by name/description) + count display "X of Y".

3. Misc fixes

  • `/marketplace/bids` page hang : subscribeToBidNotifications now short-circuits — backend SSE endpoint never existed, FastAPI was matching notifications as {bid_id} and returning 401, EventSource auto-reconnect loop made the page hang. To re-enable real-time bid notifications later, build the SSE endpoint and remove the short-circuit.
  • 'Coming up' widget polish — moved below KPI strip (was above greeting), collapse to 3 by default, fixed SchedulingDeal field name (advertiser_workspace_id / publisher_workspace_id).
  • Logo + favicon refresh (16/32/180 PNG sizes for crisp browser tabs).

Operational notes

  • No new env var dependencies beyond what was already present.
  • All migrations are reversible audience_identifiers, master_oauth_providers, property_oauth_credentials, FACEBOOK enum addition.
  • DRY-RUN activation is safe — no real platform pushes happen until Marketing API credentials are configured AND _DRY_RUN=False.
  • Token storage uses the existing AES-256-GCM helper (utils.security.encrypt_string); no new key management surface.

Future hooks (intentionally not done)

  • Real Meta / Google / TikTok Marketing API pushes — needs credentials + scope review (4-8 weeks Meta App Review for ads_management).
  • Suppression actually applied at trafficking time — trafficking layer is itself stubbed; helper is in place for the day it's wired.
  • actual_size for rule-based segments — needs identity graph + rule engine. Discussed as future Phase 1-3 of the audience roadmap; deferred until business case justifies.

---

Want to see it in your own workspace?

Bring planning, discovery, delivery, and measurement into one platform.