avlogo
Spider
Home
About
WorkSpace
Contact
Book a Call
avlogo

Aviralyadav Aviralyadav

I'm Aviral - a full-stack developer, freelancer & problem solver. Thanks for checking out my site!

Genral

  • Home
  • About Me
  • Projects

Specifics

  • Guest Book
  • Bucket List
  • Uses

Genral

  • Home
  • About Us
  • Projects

Specifics

  • Guest Book
  • Bucket List
  • Uses

Contact

  • Contact Us
  • Links
  • RSS

© 2025 Aviral Yadav. All rights reserved.

Privacy Policy•Terms of Use
Made withCodeForTravel
All Projects

Curated Work / facebook-campaign-analyzer

Facebook Campaign AnalyzerFacebook Campaign Analyzer

A Meta Ads Manager–style platform for campaigns, ad sets & ads.

NEXT.JSREACT.JSTAILWIND CSSPOSTGRESQLPRISMANODE.JSMATERIAL UIANT DESIGNAPEXCHARTS
Type
Full-Stack Dashboard
Role
Full-Stack Developer
Timeline
2024
Status
Private Project
Facebook Campaign Analyzer screenshot 1
Facebook Campaign Analyzer screenshot 2

Overview

Facebook Campaign Analyzer is a self-hosted Meta Ads Manager alternative I built to create, manage, and optimize the full campaign -> ad set -> ad hierarchy without living inside Meta's own UI. It talks directly to the Meta Marketing API to sync objects in both directions, uploads image and video creatives, and renders live performance insights — spend, CTR, CPC, ROAS — as interactive ApexCharts time series. Under the hood it's a Next.js App Router app backed by PostgreSQL and Prisma, with a delta-sync engine and a rate-limit-aware Graph API client doing the heavy lifting. The goal was a tool where a media buyer can drill from a whole account down to a single ad and trust that the numbers on screen match what Meta actually reports.

A full-stack Facebook ads management platform inspired by Meta Ads Manager — create, manage, and optimize campaigns, ad sets, and ads with image and video creatives through a secure, scalable interface.

Key Features

  • Create and manage Facebook campaigns, ad sets, and ads in a structured workflow.
  • Run image and video ads across placements using the Meta Business API.
  • Secure authentication with NextAuth for protected campaign access.
  • Real-time campaign insights and status tracking for ads and ad sets.
  • Modern, responsive dashboard UI built for performance and usability.

Why I Built This

Meta's Ads Manager is powerful but slow to navigate, and it hides the campaign -> ad set -> ad hierarchy behind endless tabs and modals. I wanted a single surface where I could see every level of an account at once, edit budgets and statuses inline, and watch performance charts update without a dozen clicks. Existing third-party dashboards were either read-only reporting tools or expensive SaaS with their own opinions, so I built the thing I actually wanted to use.

It was also the perfect excuse to work end-to-end against a genuinely hard external API. The Meta Marketing API is asynchronous in places, aggressively rate-limited, and eventually consistent — building a tool that stays in sync with it forced me to think carefully about reconciliation, background jobs, and idempotency rather than just wiring up CRUD to a friendly REST endpoint.

Architecture Overview

The app is a Next.js App Router monolith: server components and Route Handlers for anything that touches Meta, client components for the interactive tables and charts. All persistent state lives in PostgreSQL through Prisma, and the expensive Meta work — full syncs and async insight report polling — is pushed onto background workers so request handlers stay fast and never block on a rate-limited Graph API call.

facebook-campaign-analyzer/
├─ app/                       # Next.js App Router
│  ├─ (dashboard)/
│  │  ├─ campaigns/[id]/      # campaign → ad set → ad drill-down
│  │  ├─ insights/            # ApexCharts reporting views
│  │  └─ creatives/           # image + video upload manager
│  ├─ api/
│  │  ├─ meta/sync/           # pull/push sync endpoints
│  │  ├─ meta/webhooks/       # Meta change notifications
│  │  └─ insights/[level]/    # aggregated metrics API
├─ lib/
│  ├─ meta/                   # Graph API client + guards
│  │  ├─ client.ts
│  │  ├─ insights.ts          # async report jobs + polling
│  │  └─ ratelimit.ts         # BUC header parsing + backoff
│  ├─ sync/                   # delta-sync engine, upsert reconcilers
│  └─ auth.ts                 # NextAuth config
├─ prisma/
│  └─ schema.prisma           # AdAccount→Campaign→AdSet→Ad→Creative
└─ workers/                   # background sync + insight pollers
  • Frontend React server/client components with Tailwind for layout, Material UI for data-dense tables and forms, and Ant Design for the campaign tree and complex pickers; ApexCharts handles all time-series reporting.
  • Data layer PostgreSQL via Prisma, with the schema mirroring Meta's object tree one-to-one and a separate normalized metrics table for insight history.
  • Meta layer A single typed Graph API client wraps every call with token injection, BUC rate-limit accounting, and retry/backoff, so no feature code talks to Meta raw.
  • Auth NextAuth with Facebook OAuth — the same login flow that authenticates the user also yields the access token used to drive their ad accounts.
  • Background workers Sync reconciliation and async insight polling run outside the request cycle, writing results back to Postgres for the UI to read.

Modeling the Ad Hierarchy & the Sync Engine

Meta's object model is a strict tree — an Ad Account owns Campaigns, each Campaign owns Ad Sets, each Ad Set owns Ads, and each Ad points at a Creative — and I mirrored that exactly in Prisma rather than flattening it into a generic 'objects' table. Every row carries both my internal cuid and the immutable Meta object ID, plus a syncedAt timestamp and a syncStatus enum, so I can always reconcile local state against what Meta currently reports. Budgets are a subtle case: they live at either the campaign or the ad-set level depending on whether Campaign Budget Optimization is on, so I modeled budget fields as nullable at both levels and enforced the 'exactly one' rule in application code.

The sync engine is the backbone of the whole app, because everything downstream assumes local Postgres is a faithful mirror of Meta. Rather than re-crawling entire accounts, it pages the Graph API filtered by updated_time since the last successful cursor, so a routine refresh touches dozens of changed objects instead of thousands.

  • Bi-directional sync Pull reconciles Meta -> Postgres via idempotent upserts keyed on the Meta object ID; push translates local edits into Graph API mutations and reverts the optimistic UI if Meta rejects the change.
  • Delta over full crawl Each sync stores a per-account cursor and only fetches objects with an updated_time newer than the last run, keeping refreshes cheap and rate-limit-friendly.
  • Rate-limit guard A wrapper parses Meta's X-Business-Use-Case-Usage headers and throttles per ad account before hitting the ceiling, since Meta's limits are per-business-use-case, not per-endpoint.
  • Soft archival Meta never truly deletes objects — they move to ARCHIVED/DELETED — so sync flags rows inactive instead of dropping them, which preserves historical insight rows that still reference them.

Real-Time Insights with ApexCharts

Insights are the reason the tool exists, so the reporting layer had to feel live. The Marketing API won't return large insight sets synchronously — you POST an async report job, get back a report_run_id, and poll until it's ready — so I built a small job runner that kicks off report jobs, polls them with backoff, and writes normalized metric rows into a time-series table keyed by (object, level, date, breakdown).

On the frontend, ApexCharts renders spend, impressions, CTR, CPC, and ROAS as brushable area and line charts with tooltips synced across the campaign, ad-set, and ad levels. I use SWR to revalidate on an interval and on window focus so a chart reflects the latest completed sync without a manual refresh, and I downsample server-side so the browser never parses more than a few hundred points per series even on wide date ranges.

  • Async report jobs Every insight pull goes through Meta's async endpoint (report_run_id + poll), which sidesteps the synchronous endpoint's tighter limits and timeouts on larger accounts.
  • Server-side aggregation SQL rolls raw metric rows up to hourly and daily granularity so ApexCharts receives pre-shaped series instead of aggregating thousands of rows in the client.
  • Derived metrics at query time CTR, CPC, CPM, and ROAS are computed from the base fields (impressions, clicks, spend, action values) at read time, so metric definitions stay consistent everywhere they appear.
  • Synced multi-level tooltips Hovering a date on the campaign chart highlights the same point across the ad-set and ad breakdown charts, so the drill-down reads as one coherent view.

Backend Systems

The backend is Next.js Route Handlers over Prisma and PostgreSQL, with the heavy Meta work deliberately pushed onto background workers so user-facing requests stay snappy. The API is organized around the same hierarchy the UI navigates, which keeps data fetching predictable at every level.

  • Auth & tokens NextAuth with Facebook OAuth handles login and yields the user token, which I exchange for a longer-lived token; tokens are encrypted at rest and refreshed proactively before expiry so syncs never fail mid-run on an expired credential.
  • Data layer The Prisma schema mirrors the Meta tree with composite indexes on (adAccountId, metaId) for reconciliation and (objectId, date) for fast insight range scans.
  • Creative uploads Images are hashed and pushed to Meta's /adimages endpoint, which returns an image_hash I persist; videos go to /advideos as an async upload, so I poll the video's processing status and generate a local thumbnail before it can be attached to an ad.
  • API design Insight and object endpoints are namespaced by level (/api/insights/campaign, /adset, /ad), so each dashboard view maps to one predictable handler with the right aggregation baked in.
  • Background workers Sync reconciliation and insight polling run off the request path and write back to Postgres, which is what lets the UI show fresh data without ever waiting on a slow, rate-limited Graph API call inline.

Key Decisions

  • Mirror Meta's IDs, don't invent my own Every object stores its Meta object ID as the sync key, which makes upserts idempotent and reconciliation between local state and Meta trivial instead of a fuzzy matching problem.
  • Prisma over raw SQL The hierarchy is relation-heavy and I wanted typed joins across five tables; I only drop to raw SQL for the heavy insight aggregations that need window functions and date bucketing.
  • Postgres over a dedicated time-series DB Per-account insight volume is modest, so a well-indexed metrics table with date-range scans beat the operational cost of running and syncing a separate TSDB.
  • Async insight jobs, always Even for small pulls I use Meta's async report endpoint rather than the synchronous one, trading a little latency for far more headroom against rate limits and timeouts.
  • Three UI kits, scoped by strength Rather than force one library into every role, I use Material UI for dense tables and forms, Ant Design for the campaign tree and pickers, and Tailwind for layout and one-offs — accepting a heavier bundle in exchange for building each surface with the tool that fit it best.
  • Optimistic UI with server reconciliation Edits apply instantly in the UI and reconcile against Meta's actual response, reverting if rejected, because Graph API round-trips are too slow to block the user on.

What I Learned

  • Rate limits are the real architecture driver Meta's per-business-use-case throttling shaped more decisions than anything else — delta sync, async report jobs, and background workers all exist primarily to stay under the ceiling.
  • Model the vendor's constraints, not just its data Getting CBO budgets, archival states, and async job lifecycles right in the schema mattered far more than the obvious campaign/ad-set/ad fields, and skipping them early would have meant painful migrations later.
  • Eventual consistency needs a source of truth Meta doesn't always reflect a write immediately, so treating my synced Postgres mirror as the UI's source of truth — reconciled against Meta on a cadence — was the only way to keep the interface stable and honest.
  • Mixing UI libraries has a real cost Three component systems gave me the right widget for each job but a heavier bundle and occasional style clashes; if I did it again I'd standardize on one kit plus Tailwind and only reach outside it for a genuinely irreplaceable component.

Tech Stack

NEXT.JSREACT.JSTAILWIND CSSPOSTGRESQLPRISMANODE.JSMATERIAL UIANT DESIGNAPEXCHARTS

Like what you see?

Explore more work or get in touch.

More ProjectsGet in Touch