Campaign Applications (Flipped Discovery)

Adds the 4th sourcing path to the platform: creators apply to campaigns, instead of only waiting for briefs or posting listings. Advertisers publish campaigns with eligibility criteria; SOURCEs discover them in a feed and apply directly; accepting an application creates a Deal and the normal flow (chat, payment, proof) takes over.

Adds the 4th sourcing path to the platform: creators apply to campaigns, instead of only waiting for briefs or posting listings. Advertisers publish campaigns with eligibility criteria; SOURCEs discover them in a feed and apply directly; accepting an application creates a Deal and the normal flow (chat, payment, proof) takes over.

Why this matters

Before today, advertisers had three push mechanisms (briefs, marketplace bids, opportunity EOIs). SOURCEs had one real pull mechanism (post a listing, wait). This release flips the marketplace — advertisers broadcast demand, creators apply to match. It's the largest structural change to the sourcing funnel since the platform launched.

Data model

Migration a2b3c4d5e6f7 adds:

  • 10 nullable columns to campaigns (publishing toggle, deadline, eligibility criteria, per-creator budget range)
  • campaign_applications table with polymorphic FK to creator workspace + user, snapshot fields (follower count / platform / niches frozen at apply time), match_score cache, status FSM as VARCHAR(20) (matches SchedulingDeal pattern)
  • 6 indexes including partial unique (campaign_id, creator_workspace_id) WHERE status IN ('pending','shortlisted','accepted')

Migration b3c4d5e6f7a8 seeds 5 campaign_application.* IN_APP notification templates.

Backend (new router at `/campaigns/*` + `/applications/*`, behind feature flag)

| Endpoint | Role | Purpose | |---|---|---| | PATCH /campaigns/{id}/publish-applications | ADV, AGENCY | Toggle + eligibility + deadline | | GET /campaigns/feed/applications | SOURCE, SA | Paginated feed with best_fit sort, my_match dots, my_application_status | | POST /campaigns/{id}/applications | SOURCE | Submit pitch + rate + availability + sample URLs | | GET /campaigns/{id}/applications | ADV owner, SA | Advertiser inbox with score/newest/rate/rating sort | | GET /applications/mine | SOURCE | Own apps with status filter | | PATCH /applications/{id} | ADV (shortlist/decline/accept), SOURCE (withdraw) | Single dispatcher; accept spawns SchedulingDeal | | GET /applications/{id} | ADV, SOURCE, SA | Detail |

Self-protection at apply time:

  • Max 5 active applications globally per creator
  • Unique active per (campaign, creator) enforced by partial unique index
  • Restriction guard reused from existing marketplace/scheduling flows
  • Reputation: ≥1 completed deal + avg rating ≥4.0 — with new-creator exception (first 3 lifetime apps allowed without the gate)
  • SA cannot create workspace-scoped things (matches CLAUDE.md rule)

Match score formula (pure, 0–100): `` 0.40 × (rating/5) + 0.30 × eligibility_match_ratio + 0.20 × budget_fit(proposed_rate, campaign_range) + 0.10 × (0 if has_restriction_conflict else 1) ``

Persisted at create time using the rating already fetched in the reputation gate — no duplicate query. Inbox recomputes on-the-fly for null rows without persisting (read-path side effect avoided).

Accept → SchedulingDeal: wraps row in SELECT ... FOR UPDATE, creates deal with snapshotted pitch as notes and proposed_rate as agreed_price. Creator gets HIGH-priority accepted notification with deal_id in the action URL.

Notifications reuse the existing emit_event + group_key pattern from deal_chat: advertiser gets a grouped submitted notification (60-min coalescing) when applications land; creator gets direct notifications for shortlist/accept/decline/expire.

Expiry scan wired into the existing SA cron (POST /admin/analytics/run-scheduled). Flips pending/shortlisted → expired when past deadline or >30 days old. Nightly basis, not Celery.

Frontend (SOURCE side)

  • /opportunities/campaigns — feed with sort pills (Best fit / Newest / Deadline soon) + niche + platform + min-budget filters. Each card shows 5 eligibility dots and an Applied/Shortlisted badge if the creator already applied.
  • /opportunities/campaigns/[id] — single-campaign detail with full eligibility checklist, my-application status banner, and a gated Apply CTA.
  • /opportunities/applications — My Applications list with status filter pills, View/Withdraw/Open Deal actions per card.
  • ApplyToCampaignModal — property auto-selector, rate pre-fill from rate card, 50-2000 char pitch with live counter, up to 10 sample URLs, friendly error surfacing for all 4 rejection codes.
  • Sidebar: new Campaign Feed + My Applications entries for SOURCE, gated by INFLUENCER|PUBLISHER mode.

Frontend (ADVERTISER side)

  • EligibilityForm — reusable block (toggle, deadline, follower range, budget range, niches chip picker, locations chip input, platforms multi-select) with inline validation warnings.
  • /campaigns/[id]/edit — new "Creator Applications" section mounting the form; save delta-detects the eligibility section and calls publish-applications only when dirty.
  • /campaigns/[id]/applications — new Applications tab as a nested route (matches existing /adplan, /audience pattern). Inbox has sort pills, status filter, 10-sec poll, empty + closed-campaign banners.
  • ApplicationRow — avatar + pitch (3-line clamp) + eligibility dots + colored match-score badge + restriction-conflict pill + shortlist/decline/accept buttons with inline decline-reason textarea + status pill + Open Deal link when accepted.
  • _serialize_campaign extended to emit the 10 new fields so the edit form pre-fills.

Role matrix

| Role | Publish | Browse feed | Apply | View inbox | Shortlist/Accept/Decline | Withdraw | |---|---|---|---|---|---|---| | ADVERTISER (owner) | ✓ | | | own campaigns | ✓ | | | AGENCY_ADMIN | ✓ | | | own tenant's | ✓ | | | SOURCE | | ✓ | ✓ | | | own only | | VENDOR | | | | | | | | SA | (observes) | cross-tenant | ✗ (workspace-scoped) | cross-tenant | ✗ | |

Verified live

  • ADV publishes a campaign → SOURCE feed shows it with green eligibility dots
  • SOURCE applies → ADV inbox count increments, grouped notification fires
  • ADV shortlists → SOURCE notification + status badge updates
  • ADV accepts → SchedulingDeal row created, deal page loads, deal_chat works, HIGH-priority notification to creator
  • SA reads feed + inbox cross-tenant, cannot mutate (explicit 400)
  • Restriction guard fires at apply time (not just at deal creation) — creators don't get false hope
  • Feature flag disable → all new endpoints return 403

Scope deferred

  • Typing indicator on inbox
  • Fine-grained per-user read receipts (workspace-level only today)
  • Auto-matching ML / recommendation engine (simple score formula is sufficient)
  • Dedicated application-only chat thread (deal_chat spawns lazily after accept — no separate thread)
  • Bulk apply or bulk accept (one at a time)
  • Application revisions (withdraw + re-apply instead)
  • Public campaigns visible to logged-out users (auth required)
  • Pixel-level conversion tracking tied to applications (see separate "Trackable Links" proposal — not in this release)

---

Want to see it in your own workspace?

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