Three connected pieces ship together. The shared theme: stop forcing the same work to be redone in three places. Audiences, ad plans, and creator-reach analysis all become reusable workspace assets instead of per-campaign one-offs.
Ad Planner v2 — top-level page + multi-campaign plans
/ad-planner was previously reachable only from inside a campaign, and each plan belonged to exactly one campaign. The new version flips it.
- Top-level page at `/ad-planner` lists every plan in the workspace, with status, total budget, source campaign, and channel breakdown.
- Plan detail at `/ad-planner/[id]` supports plans powering multiple campaigns. Per-campaign filter chips at the top, line-items grouped by campaign.
- `LinkedAdPlansCard` on
/campaigns/[id]shows every plan touching this campaign — no more hunting through the planner module. - `PlanChooserFork` on
/campaigns/[id]/adplanempty state: pick an existing plan or create a new one. Removes the dead-end where new campaigns had no plan and no clear path to one. - Plan badges on
/campaignslist rows mark which campaigns have plans attached. - Migration `c9e8d7f6a5b4` adds
media_plans.source_campaign_idplus a backfill script that re-attaches orphan plans to campaigns by name match.
The orphan problem the migration solves: previously the planner stored items in a JSON blob on autopilot_config. Plans without items lost their campaign linkage. The new column is the durable bidirectional join.
Audience library ↔ campaign bridge
Until today, /audiences (workspace library) and the audience tab inside a campaign were unconnected surfaces. You could build a "Cart Abandoners" segment in the library, but you couldn't actually use it in a campaign without re-typing the filters.
Schema — migration d1f2e3a4b5c6 adds two thin polymorphic tables:
campaign_audience_inclusions— segment / custom_audience / lookalike entries that contribute to a campaign's reach. Logical-OR'd with the campaign's inline demographic filters.campaign_audience_exclusions— segment / custom_audience / suppression entries that remove people from reach. Applied after the inclusion union.
Both are constrained to known kinds via CHECK, unique per (campaign, source_type, source_id), and indexed for the reverse "which campaigns use this asset" lookup.
Backend — four new endpoints under /campaigns:
GET /{id}/audience/library— list inclusions + exclusions with resolved asset namesPOST /{id}/audience/library— attach an inclusion or exclusion (validates source_type per mode)DELETE /{id}/audience/library/{attachment_id}?mode=include|exclude— detachGET /audience-library/usage— workspace-scoped per-asset usage counts ({inclusions, exclusions, combined}keyed by{source_type}:{source_id}). Powers the badges on/audiences.
Frontend:
- New
AudienceLibraryPanelon/campaigns/[id]/audience— green chip strip for inclusions, red chip strip for exclusions, plus a "Build from library" right-side drawer with 4 tabs (Segments / Custom / Lookalikes / Suppression). Click any row to attach. Suppression tab attaches as exclusion; the rest as inclusions. Empty-state CTAs link back to/audiences. - "Used by N campaigns" pill on every row across all 4 tabs of
/audiences. Pulls from a single usage map fetched on mount. Closes the loop the other way: heavily-used asset → see which campaigns reference it before edit/delete.
The combined effect: build "Active Customers" suppression once → attach to every acquisition campaign with one click → never waste spend on existing customers again.
Creator Reach Analysis (📊 tab on /audiences)
A 5th tab on /audiences for advertisers planning multi-influencer campaigns: how much do these creators' audiences overlap?
Pick N influencers via search, see:
- Similarity matrix — pairwise composite score 0-100. Composite weights: niche +30, platform +20, audience age cosine 0-25, gender distribution 0-15, location 0-10, content categories 0-5.
- Per-creator profile rail — niche, platform, audience demographics, content categories at a glance.
- Pair-level explanation chips — "shares niche", "same platform", "62% audience-age overlap", etc. — so the score isn't a black box.
Use cases: avoid double-counting reach when picking 5 fitness influencers who share 70% audience; intentionally pick incrementally non-overlapping creators to broaden net reach.
Polish & gotchas fixed along the way
- Bid card restriction surfacing — bids placed against creators with active brand-category restrictions now show the conflict inline on the card, not just at submit time.
- Line-item form + CampaignWizard — Tier 1 UX cleanups: assignee picker (dropdown of workspace employees instead of UUID input), counterparty picker (searchable instead of UUID input).
- Opportunities campaign-detail page — minor layout tweaks for budget breakdown, deliverables tab.
---

