Building a Social Media Analytics Dashboard with EternalSocial API
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:
- Data Collection — Scheduled jobs that fetch social media data from the EternalSocial API
- Storage — A database that stores historical data for trend analysis
- 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 dataGET /instagram/posts/{username}— Instagram feed postsGET /tiktok/profile/{username}— TikTok profile dataGET /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.