Peergrowth Pulse
By Anindo Neel Dutta

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
| Outcome | Result |
|---|---|
| Leads processed | ~2,000 |
| Manual lead passing | Gone |
| Manual client reporting | Gone |
| Report creation | Hours -> ~1 minute |
The problem
Same loop, every account:
- Performance lived in Ads Manager: spend, delivery, creatives, pacing.
- Submissions lived in Instant Forms: who filled the form, and when.
- Follow-up meant passing leads between people and tools by hand.
- Reporting meant assembling numbers into something presentable.
- “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:
- Gather Meta + pipeline data for the range
- One narrative pass
- Freeze a JSON snapshot
- 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
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
| Area | Implementation |
|---|---|
| Frontend | Next.js 16 App Router, React 19 |
| Auth | Clerk (Google + Facebook); Facebook OAuth token -> Meta Marketing API |
| Database | Postgres + Drizzle ORM; row-level security for tenant isolation |
| Jobs | Trigger.dev: hourly scheduled lead sync, manual/first-setup sync |
| AI | Gemini 2.5 Flash; 11 read-only tools over Meta + Postgres |
| Observability | Sentry (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
| Claim | Evidence | Confidence |
|---|---|---|
| Live Meta insights | Meta Graph fetch layer + dashboard meta-services | High |
| Hourly Instant Form sync | Trigger.dev scheduled lead sync + lead upsert module | High |
| CRM fields preserved on sync | Conflict update omits workflow/notes/custom attributes | High |
| Capacity-matching attribution | Lead attribution enrichment against ad-day Meta actions | High |
| Freeze-once public reports | Shared report snapshot + /r/[token] public route | High |
| Tool-grounded Copilot | System prompt + tool registry; Gemini 2.5 Flash config | High |
| Multi-tenant RLS | Org/ACL schema + RLS migrations + cross-tenant tests | High |
| ~2,000 leads processed | Verified business outcome (agency operations) | High |
| Report time: few hours -> ~1 minute | Verified business outcome (agency operations) | High |
| Manual lead passing / report creation replaced | Verified business outcome (agency operations) | High |
