Back to Blog

Building a Social Media Analytics Dashboard with EternalSocial API

By EternalSocial Team4 {minutes} min read

Social media analytics dashboards are one of the most common applications built on top of social data APIs. Whether you're tracking your own brand, monitoring competitors, or building an analytics product for clients, the architecture is the same: fetch data, store it, and visualize it.

This guide walks through building a complete social media analytics dashboard using the EternalSocial API. We'll cover data fetching, database storage, and visualization — with TypeScript code examples at every step.

Architecture Overview

The dashboard has three layers:

  1. Data Collection — Scheduled jobs that fetch social media data from the EternalSocial API
  2. Storage — A database that stores historical data for trend analysis
  3. Visualization — A frontend that queries the database and renders charts
EternalSocial API → Fetch Script → Database → Dashboard UI
     (data)          (cron job)    (Postgres)   (charts)

You can build this with any tech stack. We'll use TypeScript throughout, with PostgreSQL for storage and any charting library of your choice for visualization.

Step 1: Set Up Data Fetching

First, create a data fetching module that pulls profile and post data from the EternalSocial API.

API Client

const API_BASE = "https://api.eternalsocial.dev/v1";
const API_KEY = process.env.ETERNALSOCIAL_API_KEY;

interface ApiResponse<T> {
  data: T;
  pagination?: {
    nextCursor: string | null;
  };
}

async function apiGet<T>(path: string): Promise<ApiResponse<T>> {
  const response = await fetch(`${API_BASE}${path}`, {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
    },
  });

  if (response.status === 429) {
    const retryAfter = Number(response.headers.get("Retry-After")) || 10;
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
    return apiGet<T>(path);
  }

  if (!response.ok) {
    throw new Error(`API error: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

Fetch Profile Data

interface ProfileSnapshot {
  username: string;
  platform: "instagram" | "tiktok";
  followers: number;
  following: number;
  postsCount: number;
  fetchedAt: Date;
}

async function fetchInstagramProfile(
  username: string
): Promise<ProfileSnapshot> {
  const { data } = await apiGet<{
    username: string;
    followers: number;
    following: number;
    postsCount: number;
  }>(`/instagram/profile/${username}`);

  return {
    username: data.username,
    platform: "instagram",
    followers: data.followers,
    following: data.following,
    postsCount: data.postsCount,
    fetchedAt: new Date(),
  };
}

async function fetchTikTokProfile(username: string): Promise<ProfileSnapshot> {
  const { data } = await apiGet<{
    username: string;
    followers: number;
    following: number;
    videoCount: number;
  }>(`/tiktok/profile/${username}`);

  return {
    username: data.username,
    platform: "tiktok",
    followers: data.followers,
    following: data.following,
    postsCount: data.videoCount,
    fetchedAt: new Date(),
  };
}

Fetch Recent Posts

interface PostSnapshot {
  postId: string;
  username: string;
  platform: "instagram" | "tiktok";
  likes: number;
  comments: number;
  views: number | null;
  caption: string | null;
  publishedAt: string;
  fetchedAt: Date;
}

async function fetchInstagramPosts(
  username: string,
  limit = 20
): Promise<PostSnapshot[]> {
  const { data } = await apiGet<
    Array<{
      shortcode: string;
      likes: number;
      comments: number;
      caption: string;
      timestamp: string;
    }>
  >(`/instagram/posts/${username}?limit=${limit}`);

  return data.map((post) => ({
    postId: post.shortcode,
    username,
    platform: "instagram",
    likes: post.likes,
    comments: post.comments,
    views: null,
    caption: post.caption,
    publishedAt: post.timestamp,
    fetchedAt: new Date(),
  }));
}

async function fetchTikTokPosts(
  username: string,
  limit = 20
): Promise<PostSnapshot[]> {
  const { data } = await apiGet<
    Array<{
      id: string;
      likeCount: number;
      commentCount: number;
      viewCount: number;
      description: string;
      createTime: string;
    }>
  >(`/tiktok/posts/${username}?limit=${limit}`);

  return data.map((video) => ({
    postId: video.id,
    username,
    platform: "tiktok",
    likes: video.likeCount,
    comments: video.commentCount,
    views: video.viewCount,
    caption: video.description,
    publishedAt: video.createTime,
    fetchedAt: new Date(),
  }));
}

Step 2: Store Data in a Database

We'll use PostgreSQL, but any relational database works. Here's the schema and insertion logic.

Database Schema

CREATE TABLE profile_snapshots (
  id SERIAL PRIMARY KEY,
  username VARCHAR(255) NOT NULL,
  platform VARCHAR(50) NOT NULL,
  followers INTEGER NOT NULL,
  following INTEGER NOT NULL,
  posts_count INTEGER NOT NULL,
  fetched_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE post_snapshots (
  id SERIAL PRIMARY KEY,
  post_id VARCHAR(255) NOT NULL,
  username VARCHAR(255) NOT NULL,
  platform VARCHAR(50) NOT NULL,
  likes INTEGER NOT NULL,
  comments INTEGER NOT NULL,
  views INTEGER,
  caption TEXT,
  published_at TIMESTAMP WITH TIME ZONE,
  fetched_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Indexes for common queries
CREATE INDEX idx_profile_username_platform ON profile_snapshots (username, platform);
CREATE INDEX idx_profile_fetched_at ON profile_snapshots (fetched_at);
CREATE INDEX idx_post_username_platform ON post_snapshots (username, platform);
CREATE INDEX idx_post_fetched_at ON post_snapshots (fetched_at);

Insert Functions

Using a simple PostgreSQL client like pg:

import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

async function saveProfileSnapshot(snapshot: ProfileSnapshot): Promise<void> {
  await pool.query(
    `INSERT INTO profile_snapshots (username, platform, followers, following, posts_count, fetched_at)
     VALUES ($1, $2, $3, $4, $5, $6)`,
    [
      snapshot.username,
      snapshot.platform,
      snapshot.followers,
      snapshot.following,
      snapshot.postsCount,
      snapshot.fetchedAt,
    ]
  );
}

async function savePostSnapshots(snapshots: PostSnapshot[]): Promise<void> {
  for (const snapshot of snapshots) {
    await pool.query(
      `INSERT INTO post_snapshots (post_id, username, platform, likes, comments, views, caption, published_at, fetched_at)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
      [
        snapshot.postId,
        snapshot.username,
        snapshot.platform,
        snapshot.likes,
        snapshot.comments,
        snapshot.views,
        snapshot.caption,
        snapshot.publishedAt,
        snapshot.fetchedAt,
      ]
    );
  }
}

Step 3: Schedule Data Collection

Run the data fetching on a schedule. A simple cron job that runs every 6 hours works well for most dashboards.

Collection Script

const TRACKED_ACCOUNTS = [
  { username: "yourcompany", platform: "instagram" as const },
  { username: "competitor1", platform: "instagram" as const },
  { username: "competitor2", platform: "instagram" as const },
  { username: "yourcompany", platform: "tiktok" as const },
  { username: "competitor1", platform: "tiktok" as const },
];

async function collectData(): Promise<void> {
  console.log(`Starting data collection at ${new Date().toISOString()}`);

  for (const account of TRACKED_ACCOUNTS) {
    try {
      // Fetch and save profile
      const profile =
        account.platform === "instagram"
          ? await fetchInstagramProfile(account.username)
          : await fetchTikTokProfile(account.username);
      await saveProfileSnapshot(profile);

      // Fetch and save recent posts
      const posts =
        account.platform === "instagram"
          ? await fetchInstagramPosts(account.username, 10)
          : await fetchTikTokPosts(account.username, 10);
      await savePostSnapshots(posts);

      console.log(
        `Collected data for @${account.username} (${account.platform})`
      );

      // Small delay between accounts to respect rate limits
      await new Promise((resolve) => setTimeout(resolve, 2000));
    } catch (error) {
      console.error(
        `Failed to collect data for @${account.username}:`,
        error instanceof Error ? error.message : error
      );
    }
  }

  console.log("Data collection complete");
}

collectData();

Run this with a cron job or a task scheduler:

# Run every 6 hours
0 */6 * * * cd /path/to/project && npx tsx collect.ts >> /var/log/social-collector.log 2>&1

Step 4: Build Dashboard Queries

Now that you have historical data, build queries to power your dashboard views.

Follower Growth Over Time

interface GrowthDataPoint {
  date: string;
  followers: number;
}

async function getFollowerGrowth(
  username: string,
  platform: string,
  days: number
): Promise<GrowthDataPoint[]> {
  const result = await pool.query(
    `SELECT
       DATE(fetched_at) as date,
       MAX(followers) as followers
     FROM profile_snapshots
     WHERE username = $1
       AND platform = $2
       AND fetched_at > NOW() - INTERVAL '1 day' * $3
     GROUP BY DATE(fetched_at)
     ORDER BY date ASC`,
    [username, platform, days]
  );

  return result.rows.map((row) => ({
    date: row.date,
    followers: row.followers,
  }));
}

Engagement Rate by Post

interface EngagementDataPoint {
  postId: string;
  engagementRate: number;
  publishedAt: string;
}

async function getPostEngagement(
  username: string,
  platform: string,
  limit: number
): Promise<EngagementDataPoint[]> {
  const result = await pool.query(
    `SELECT DISTINCT ON (post_id)
       post_id,
       likes,
       comments,
       views,
       published_at
     FROM post_snapshots
     WHERE username = $1 AND platform = $2
     ORDER BY post_id, fetched_at DESC
     LIMIT $3`,
    [username, platform, limit]
  );

  return result.rows.map((row) => ({
    postId: row.post_id,
    engagementRate:
      row.views > 0 ? ((row.likes + row.comments) / row.views) * 100 : 0,
    publishedAt: row.published_at,
  }));
}

Competitive Comparison

interface CompetitorComparison {
  username: string;
  platform: string;
  currentFollowers: number;
  followerChange: number;
  avgLikes: number;
  avgComments: number;
}

async function getCompetitorComparison(
  usernames: string[],
  platform: string
): Promise<CompetitorComparison[]> {
  const comparisons: CompetitorComparison[] = [];

  for (const username of usernames) {
    const profileResult = await pool.query(
      `SELECT
         (SELECT followers FROM profile_snapshots
          WHERE username = $1 AND platform = $2
          ORDER BY fetched_at DESC LIMIT 1) as current_followers,
         (SELECT followers FROM profile_snapshots
          WHERE username = $1 AND platform = $2
          AND fetched_at < NOW() - INTERVAL '7 days'
          ORDER BY fetched_at DESC LIMIT 1) as week_ago_followers`,
      [username, platform]
    );

    const engagementResult = await pool.query(
      `SELECT
         AVG(likes) as avg_likes,
         AVG(comments) as avg_comments
       FROM (
         SELECT DISTINCT ON (post_id) likes, comments
         FROM post_snapshots
         WHERE username = $1 AND platform = $2
         ORDER BY post_id, fetched_at DESC
         LIMIT 20
       ) recent_posts`,
      [username, platform]
    );

    const row = profileResult.rows[0];
    const engagement = engagementResult.rows[0];

    comparisons.push({
      username,
      platform,
      currentFollowers: row?.current_followers || 0,
      followerChange:
        (row?.current_followers || 0) - (row?.week_ago_followers || 0),
      avgLikes: Math.round(engagement?.avg_likes || 0),
      avgComments: Math.round(engagement?.avg_comments || 0),
    });
  }

  return comparisons;
}

Step 5: Visualize the Data

With the query functions above, you can build a dashboard frontend with any framework. Here's a conceptual example using a React component:

// Dashboard component pseudocode
interface DashboardProps {
  username: string;
  competitors: string[];
  platform: "instagram" | "tiktok";
}

async function DashboardPage({ username, competitors, platform }: DashboardProps) {
  // Fetch data server-side
  const followerGrowth = await getFollowerGrowth(username, platform, 30);
  const postEngagement = await getPostEngagement(username, platform, 20);
  const comparison = await getCompetitorComparison(
    [username, ...competitors],
    platform
  );

  return (
    <div>
      <h1>@{username} Analytics</h1>

      {/* Follower growth line chart */}
      <section>
        <h2>Follower Growth (Last 30 Days)</h2>
        <LineChart data={followerGrowth} xKey="date" yKey="followers" />
      </section>

      {/* Engagement rate bar chart */}
      <section>
        <h2>Post Engagement Rates</h2>
        <BarChart data={postEngagement} xKey="postId" yKey="engagementRate" />
      </section>

      {/* Competitor comparison table */}
      <section>
        <h2>Competitive Landscape</h2>
        <table>
          <thead>
            <tr>
              <th>Account</th>
              <th>Followers</th>
              <th>7-Day Change</th>
              <th>Avg Likes</th>
              <th>Avg Comments</th>
            </tr>
          </thead>
          <tbody>
            {comparison.map((c) => (
              <tr key={c.username}>
                <td>@{c.username}</td>
                <td>{c.currentFollowers.toLocaleString()}</td>
                <td>{c.followerChange > 0 ? "+" : ""}{c.followerChange.toLocaleString()}</td>
                <td>{c.avgLikes.toLocaleString()}</td>
                <td>{c.avgComments.toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </section>
    </div>
  );
}

The specific charting library doesn't matter — Recharts, Chart.js, D3, Nivo, or Tremor all work fine. The important thing is that your data layer is clean and your queries return well-structured data.

Extending the Dashboard

Once the foundation is in place, you can add more views:

Hashtag Performance

Track which hashtags drive the most engagement by extracting them from post captions and correlating with like/comment counts.

Posting Schedule Analysis

Analyze which days and times produce the highest engagement by grouping post performance by day-of-week and hour.

Content Type Breakdown

Compare performance across content types — feed posts vs. reels on Instagram, or short vs. long videos on TikTok.

Alerts and Notifications

Set up threshold-based alerts: notify when a competitor gains followers faster than usual, or when your engagement rate drops below a target.

Production Considerations

Before deploying your dashboard, consider these operational details:

Data Retention

Profile snapshots add up. If you track 10 accounts and fetch every 6 hours, that's 40 rows per day, or ~14,600 per year. Post snapshots grow faster. Plan your retention policy — most teams keep 90 days of granular data and roll up older data into daily/weekly aggregates.

Error Recovery

Your collection script should be idempotent. If it fails halfway through, running it again shouldn't create duplicates or miss accounts. Use ON CONFLICT clauses or check for existing snapshots before inserting.

API Budget

Plan your API usage based on your collection frequency and the number of tracked accounts. Each collection cycle uses one profile request and one posts request per account. For 10 accounts every 6 hours, that's about 80 requests per day, or 2,400 per month — well within the Starter plan.

API Reference

For complete endpoint documentation, see the API docs.

Key endpoints used in this guide:

  • GET /instagram/profile/{username} — Instagram profile data
  • GET /instagram/posts/{username} — Instagram feed posts
  • GET /tiktok/profile/{username} — TikTok profile data
  • GET /tiktok/posts/{username} — TikTok video posts

Get Started

Building a social media analytics dashboard doesn't have to start with weeks of scraping infrastructure. With the EternalSocial API, you can go from zero to collecting real data in minutes.

Sign up for a free trial to get your API key. The free tier includes 1,000 requests per month — more than enough to prototype your dashboard with real data before scaling up.

For full endpoint documentation and response schemas, check the API reference.