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_providerstable (migrationd5e6f7a8b9c0) — 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
e6f7a8b9c0d1addsapp_url+redirect_uricolumns. Migrationa8b9c0d1e2f3seeds 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_service— Instagram-with-Instagram-Login flow (api.instagram.com). Bypasses Facebook Page mediation — works without an FB Page linkage. Usesinstagram_business_basicscope.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.pynow reads Google OAuth credentials frommaster_oauth_providers.youtuberow first, falls back to env vars. Single source of truth — same Google project serves both YouTube Connect AND Sign-in-with-Google.FRONTEND_URLconfigurable via env (was hardcoded tolocalhost:3005).- Fixed
create_access_token()call signature mismatch in OAuth path. - Fixed
family_idNULL 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
/loginare now hidden behind aSHOW_SOCIAL_LOGINflag (defaultfalse) until OAuth apps clear platform review.
2. Audience Module Hardening
#### CSV upload + hashing
- New
audience_identifierstable (migrationc0d1e2f3a4b5) — 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}/uploadandPOST /audience/suppression/{id}/upload— multipart endpoints. 50 MB cap, dedupe via unique constraint, return summary{raw_rows, hashed, deduped, skipped}.- New
CsvUploadModalcomponent — drag-drop, format hint, sample.csv download, identifier-type override, results display. - Sample file at
/public/sample-audience.csvfor download from the modal.
#### Suppression query helper
services/audience/suppression_query.py—get_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 anaudience_activation_logrow, marks status as ACTIVE.- New
audience_activation_logtable — audit trail of every push attempt (queued / started / dry_run / success / failed) with payload summary. POST /audience/activatenow enqueues a realBackgroundTaskthat drivesSegmentActivationthroughPENDING → SYNCING → ACTIVE / FAILED.GET /audience/segments/{id}/activationsreflects 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 = Falseat the top ofplatform_activator.pyand 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-kpisGET 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 :
subscribeToBidNotificationsnow short-circuits — backend SSE endpoint never existed, FastAPI was matchingnotificationsas{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
SchedulingDealfield 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_sizefor rule-based segments — needs identity graph + rule engine. Discussed as future Phase 1-3 of the audience roadmap; deferred until business case justifies.
---

