Case studies, not a tech stack list.
Selected work with the problem, approach, and outcome behind it.
// loading projects...
Selected work with the problem, approach, and outcome behind it.
// loading projects...
Analytics platform for Instagram, TikTok, and Threads creators and agencies. Track performance, spot trends, and manage multiple profiles. All from one dashboard.
Agencies managing dozens of creator accounts had no single view of performance across platforms. Data lived in spreadsheets, exports were manual, and decisions were made days after the trend had already passed.
Started with a handful of agencies as design partners before writing a line of dashboard code. Their workflows shaped the data model, especially the split between tracking your own accounts and researching competitors on the same underlying profile data. Background sync runs as a daily batch job (Trigger.dev): one cron finds every stale profile belonging to an active subscriber, then fans out per-profile jobs that fetch new posts and compute engagement deltas. The stack (Next.js, Prisma, Zustand) was chosen to keep iteration fast while the product direction was still moving weekly.
Now used by agencies to monitor millions of followers and views across managed profiles, replacing manual spreadsheet reporting with a live dashboard.
Context: A single job walking every tracked profile in sequence would either time out or take hours as the profile count grew, and one slow or failing profile would risk stalling the entire run for everyone.
Decision: Split sync into two job types: one lightweight daily job that only finds stale profiles belonging to active subscribers, and a separate per-profile job, triggered once per profile found, that does the actual fetch-and-compute work.
Consequence: Adding the thousandth tracked profile doesn't change how fast the first 999 sync. One profile's API failure or timeout stays isolated to that profile instead of blocking or corrupting anyone else's data for the day.
Context: Agencies wanted to track their own clients' accounts. The same users also wanted to track competitor accounts for research, without mixing the two into one undifferentiated list.
Decision: Modeled both as the same underlying profile, linked through two separate grouping entities (one for general tracking, one for research) with independent tags and subscription limits, instead of duplicating the profile model or bolting a flag onto one table.
Consequence: Every analytics, filtering, and metrics feature built for one context works for the other automatically. The cost is an extra layer of indirection that has to be kept in mind on every profile-related query.
Context: Analytics pages pull the same underlying data (a user's creators, their linked profiles, tag associations) from several different routes and components. Without discipline, that turns into the same expensive query written slightly differently in five places, each with its own caching, or none at all.
Decision: Every database read goes through a named, cached query helper (queryCreators, queryCreatorsWithProfiles, and similar), enforced by a documented rule against calling Prisma directly from a route handler or component.
Consequence: A caching fix or query optimization made once applies everywhere that data gets used. The cost is an extra function to write for every new query shape, instead of just calling Prisma inline and moving on.