Back to case studies
Product Build··8 min read

Peergrowth Pulse

By Anindo Neel Dutta

Peergrowth Pulse

Our Meta growth ops were a mess of handoffs.

Ads Manager for spend. Instant Forms for leads. Spreadsheets for status. Another round of screenshots and narrative every time a client asked for a report. Useful data everywhere. A usable workflow nowhere.

So we built Pulse for ourselves. Not a product brief. A workflow we were tired of running by hand.

About 2,000 leads have gone through it. Client report creation went from at least a few hours to about a minute.

Results

OutcomeResult
Leads processed~2,000
Manual lead passingGone
Manual client reportingGone
Report creationHours -> ~1 minute

The problem

Same loop, every account:

  1. Performance lived in Ads Manager: spend, delivery, creatives, pacing.
  2. Submissions lived in Instant Forms: who filled the form, and when.
  3. Follow-up meant passing leads between people and tools by hand.
  4. Reporting meant assembling numbers into something presentable.
  5. “Why did CPL move?” meant starting over.

The data wasn’t missing. Meta had metrics. Forms had submissions. The team had judgment. What was missing was the glue: acquisition -> leads -> pipeline -> report, without people as middleware.

Why we built it

Not another analytics dashboard.

Software shaped like how we actually run accounts:

  • Meta-first: Facebook and Instagram ads + Instant Form leads. Not a thin multi-channel skin.
  • Lead-gen focused: volume, CPL, spam, qualification, and won outcomes in the same loop as spend.
  • Agency workspaces: teams, roles, ad-account access. No shared Ads Manager logins.
  • A real lead pipeline: statuses, notes, attributes that survive Meta re-syncs.
  • Attribution honesty: when form volume and Meta’s lead actions disagree, show it.
  • Frozen client reports: numbers that don’t rewrite themselves after you send the link.
  • Grounded AI: answers from live account data, not invented CPL.

Off-the-shelf tools each cover a piece. Pulse exists because those pieces had to run as one workflow: the one we were already doing manually.

Before vs after

Before

Meta Ads Manager
  -> lead exports
  -> manual lead passing
  -> spreadsheet / CRM
  -> manually assembled report
  -> client

After

Meta
  -> Pulse
  -> lead pipeline
  -> attribution
  -> analysis (dashboard + Copilot)
  -> frozen client report link

Report creation: hours -> ~1 minute.
Lead handoff: manual -> one Pulse workflow.

Pulse doesn’t ask anyone to “check another dashboard.” It kills the handoffs between Ads Manager, form exports, and the deck.

What shipped

Invite-only software we use to run Meta acquisition end to end.

Live Meta analytics
Overview KPIs, campaigns, ad sets (including pacing), creatives, funnels, period compares from the Meta Marketing API. Current delivery, not a stale warehouse copy.

Instant Form lead pipeline
Leads sync into Postgres on a schedule (hourly for leads-goal accounts), with first-setup backfill and manual sync. Operators update status, notes, custom attributes. Meta re-syncs refresh Graph fields and attribution without wiping CRM work.

Attribution
Campaign/ad context from Meta plus an explicit attribution status, including when form volume and Meta’s ad-level lead actions don’t line up.

Client reports
Pull live data once, generate a narrative, freeze a snapshot, share a public link. No client login. Revoke or restore when needed.

Workspaces
Orgs, roles, per-ad-account access. Agency work stays isolated.

AI Copilot
Read-only analyst on the same Meta + Postgres data the dashboard uses. Designed so it can’t invent a client’s CPL.

Goal modes
Per ad account: leads (CPL) or sales (ROAS), with targets and breach alerts.

The hard parts

Spreadsheets never force you to name these.

Two clocks of freshness

Spend needs to feel live. Leads need durable operator state: status, notes, tags.

So Pulse reads Meta insights live and syncs Instant Forms into Postgres, preserving CRM fields on refresh. Cache-everything fails one side. Fetch-everything-live fails the other. Both clocks, on purpose.

Attribution ambiguity

Form rows often include campaign/ad fields. Meta also reports lead actions at the ad level by day. Those counts are not one-to-one. Organic submissions, missing ad IDs, over-subscription happen in real accounts.

Pulse doesn’t pretend every form lead with an ad ID is cleanly attributed. It capacity-matches form submissions against Meta’s ad-day lead actions and labels leads attributed, not attributed, organic, or unknown. Operators see the mess. They don’t inherit a fake CPL story.

Multi-tenant isolation

Agency software is security work, not just charts. Org membership, per-ad-account grants, Postgres RLS. Application checks and database policies both enforce the boundary. One workspace’s leads don’t leak into another.

Report integrity

Live Meta numbers move. A “live” client dashboard tomorrow can make yesterday’s call look wrong.

Pulse freezes a snapshot at creation and serves it on a public token link. Not forever-fresh for clients. A stable artifact of what you sent.

AI Copilot

We didn’t want an assistant that invents CPL with confidence.

So Copilot is constrained:

  • Gemini 2.5 Flash
  • Answers only through read-only tools: the same surfaces the product uses (overview, insights, period compare, leads, attribution funnel, cohorts, change detection, sync health, lead detail, follow-up suggestions)
  • Numbers must come from those tools, not model guesswork
  • Lead context is compacted with PII stripped before it hits the model
  • Briefs and report narratives use structured outputs
  • Usage is rate-limited

Follow-up suggestions stay suggestions. Copilot does not silently rewrite lead status. Humans own CRM changes.

Reporting

The workflow is boring on purpose:

  1. Gather Meta + pipeline data for the range
  2. One narrative pass
  3. Freeze a JSON snapshot
  4. Share /r/[token]

Clients get scorecards, trends, funnels, campaigns, creatives, and when relevant, pipeline and attribution. No contact dumps. Need the link dead? Revoke it.

That’s how report creation went from hours of assembly to about a minute of generate-and-send.

Architecture

Rendering diagram…

Stack: Next.js (App Router), React, Clerk (Facebook OAuth for Meta Marketing API tokens), Drizzle + Postgres, Trigger.dev for lead sync, Gemini via Vercel AI SDK, Sentry.

Data split: ads performance live from Meta; Instant Form leads + CRM state in Postgres; reports freeze a point-in-time snapshot.

What we’d do differently

Deliberate constraints, not accidents:

  • Meta-only: depth for Facebook/Instagram lead gen over shallow multi-channel. Google Ads is out of scope for now.
  • Pipeline, not full CRM: Pulse owns Meta lead workflow. Deeper HubSpot/Zoho connectors are future work, not live.
  • AI polish debt: grounding and tooling are solid; small prompt assumptions (currency framing, etc.) still need cleanup as usage widens.

We wouldn’t reverse the bet: build the workflow we needed, then harden the hard parts: sync, attribution, tenancy, freeze-once reports, grounded AI.

The point

Peergrowth isn’t just an agency that runs ads.

Pulse is how we think about growth ops: acquisition, attribution, reporting clients can trust, infrastructure, automation, and AI that’s forced to stay honest, wired into one workflow.

We built it to kill our own problem first. ~2,000 leads later, with reports down to about a minute, it’s the receipt.

Not every client problem looks like Pulse. When growth ops need custom sync, attribution, reporting, automation, or grounded AI, we’ve already done that work on our own stack.

Technical details

Stack

AreaImplementation
FrontendNext.js 16 App Router, React 19
AuthClerk (Google + Facebook); Facebook OAuth token -> Meta Marketing API
DatabasePostgres + Drizzle ORM; row-level security for tenant isolation
JobsTrigger.dev: hourly scheduled lead sync, manual/first-setup sync
AIGemini 2.5 Flash; 11 read-only tools over Meta + Postgres
ObservabilitySentry (including AI telemetry), structured AI logs

Copilot tools

get_ad_overview, query_meta_insights, compare_periods, query_leads, get_attribution_funnel, analyze_cohort, detect_changes, get_account_targets, get_sync_health, get_lead_detail, suggest_lead_follow_up

Sync & attribution

  • First lead sync backfills ~90 days; incremental sync continues from last success
  • Upserts key on Meta lead identity; workflow status, notes, and custom attributes are preserved
  • Attribution capacity-matches form leads to Meta ad-level lead actions per account-timezone day
  • Scheduled sync targets accounts in leads outcome mode

Security

  • Invite-gated platform access
  • Organization roles + per-ad-account ACL
  • Application access asserts plus Postgres RLS
  • PII stripped from Copilot/report aggregates; public reports omit contact fields

Evidence

ClaimEvidenceConfidence
Live Meta insightsMeta Graph fetch layer + dashboard meta-servicesHigh
Hourly Instant Form syncTrigger.dev scheduled lead sync + lead upsert moduleHigh
CRM fields preserved on syncConflict update omits workflow/notes/custom attributesHigh
Capacity-matching attributionLead attribution enrichment against ad-day Meta actionsHigh
Freeze-once public reportsShared report snapshot + /r/[token] public routeHigh
Tool-grounded CopilotSystem prompt + tool registry; Gemini 2.5 Flash configHigh
Multi-tenant RLSOrg/ACL schema + RLS migrations + cross-tenant testsHigh
~2,000 leads processedVerified business outcome (agency operations)High
Report time: few hours -> ~1 minuteVerified business outcome (agency operations)High
Manual lead passing / report creation replacedVerified business outcome (agency operations)High
Next step

Still running this workflow by hand?

If you're still stitching this workflow together by hand, talk to us. We'll tell you what we'd build, and what we wouldn't.