How I built a dual-path synchronization architecture for 7 VS Code extensions using VS Marketplace & Open VSX APIs, 24-hour client caching, and daily zero-dependency GitHub Actions.
As an open-source creator building a growing ecosystem of developer tools, one repetitive chore kept draining my time: keeping metrics and version numbers in sync.
My portfolio, dotUniverse, showcases 20+ open-source toolsβincluding 7 VS Code extensions published across both the Visual Studio Marketplace and the Eclipse Open VSX Registry.
Every time I bumped a release or checked how many developers installed CodeTune, dotcommand, or DotShare, I had to:
- Log into multiple web dashboards.
- Manually calculate cross-platform sums.
- Edit HTML cards, version badges, and repository README tables by hand.
Manual upkeep is tedious, error-prone, and quickly falls behind. I wanted an automated, dual-path synchronization architecture:
- At Runtime: Visitors see fresh, API-backed numbers and versions fetched directly from store APIs, cached intelligently on the client for 24 hours.
-
At Build/Repository Time: Static fallbacks in
index.htmland markdown tables inREADME.mdare automatically kept fresh via a scheduled, zero-dependency GitHub Action.
Here is a deep dive into how I built this system using pure modern JavaScript, 24-hour client-side caching, and GitHub Actions CI/CD.
ποΈ The Architecture: Dual-Path Synchronization
Instead of treating this as a simple "fetch data in JavaScript" problem, I designed a dual-path synchronization pipeline sharing a single source of truth:
ββββββββββββββββββββββββββββββββ
β Visual Studio Marketplace β
β & Open VSX Registry APIs β
ββββββββββββββββ¬ββββββββββββββββ
β
βββββββββββββββββββββ΄ββββββββββββββββββββ
β β
[Runtime Path] [Build/CI Path]
β β
βΌ βΌ
extension-stats.js sync-extension-stats.mjs
β β
ββββββββ΄βββββββ ββββββββ΄βββββββ
βΌ βΌ βΌ βΌ
24h Cache Fresh DOM index.html README.md
(localStorage) (No Flicker) (Fallback) (Markdown)
β
βΌ
Git Commit & Push
(Only on Changes)
This decoupled design guarantees that:
-
First-time and repeat visitors get an instant render from static markup or
localStoragewithout waiting for API responses to render the initial content. - Search engine crawlers and developers browsing the GitHub repository see accurate static numbers even without executing client-side JavaScript.
ποΈ The Challenge: Two Stores, Fragmented Metrics
Tracking extensions across the VS Code ecosystem is fragmented:
- VS Code Marketplace: Owned by Microsoft. Its web interface displays a single "Installs" counter, but its underlying gallery API exposes separate metrics.
- Open VSX Registry: The vendor-neutral marketplace used by VSCodium, Gitpod, and Eclipse Theia. It provides a clean REST API with different metric structures.
Crucially, both APIs support CORS with access-control-allow-origin: *, allowing modern browsers to query them directly without requiring a proxy server.
π Layer 1: Querying Store APIs Directly
1. Batch-Querying Visual Studio Marketplace
Instead of making 7 separate HTTP requests, Microsoft's Marketplace API allows querying multiple extensions in a single POST request to _apis/public/gallery/extensionquery:
async function fetchVsMarketplace(extensions) {
const body = {
filters: [{
criteria: extensions.map(ext => ({ filterType: 7, value: ext.vsId }))
}],
flags: 914 // Requests statistics, versions, and metadata
};
const res = await fetch('https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json;api-version=3.0-preview.1'
},
body: JSON.stringify(body)
});
const json = await res.json();
const map = {};
for (const ext of json?.results?.[0]?.extensions || []) {
const stats = {};
for (const s of ext.statistics || []) {
stats[s.statisticName] = s.value;
}
// Empirical verification: Microsoft Marketplace UI displays (install + downloadCount)
const installs = Math.round((stats.install || 0) + (stats.downloadCount || 0));
// The API returns the latest release first in versions[0]
const version = ext.versions?.[0]?.version || null;
map[ext.extensionName.toLowerCase()] = { installs, version };
}
return map;
}
π‘ Empirical Verification on Marketplace Installs: Microsoft's API returns two separate download metrics:
install(active installed instances) anddownloadCount(package downloads/updates). Through empirical cross-referencing between the API response and the live web pages for all 7 extensions, the "Installs" figure shown on the Marketplace product page aligned with(stats.install + stats.downloadCount). This was observed across the seven extensions tested during development; it should be treated as an empirical observation rather than a guaranteed API contract.
2. Querying Open VSX Registry
Open VSX provides an open REST endpoint at https://open-vsx.org/api/{namespace}/{extension}:
async function fetchOpenVsx(extensions) {
const map = {};
const ovsxExts = extensions.filter(e => e.ovsxId);
const promises = ovsxExts.map(async (ext) => {
try {
const res = await fetch(`https://open-vsx.org/api/${ext.ovsxId}`);
if (!res.ok) return;
const data = await res.json();
map[ext.key] = {
downloads: data.downloadCount || 0,
version: data.version || null
};
} catch (err) {
console.warn(`Open VSX fetch failed for ${ext.key}:`, err);
}
});
await Promise.allSettled(promises);
return map;
}
β‘ Layer 2: The Client-Side Module (extension-stats.js)
In dotUniverse's modular architecture, we created js/modules/extension-stats.js.
Why a 24-Hour Cache is Essential
Firing external API requests on every page refresh is bad engineeringβit wastes bandwidth, risks third-party rate limits, and causes text flicker.
Because developer tools are typically released over days or weeks, a 24-hour cache TTL (24 * 60 * 60 * 1000) is a practical default: it reduces unnecessary API requests while keeping the displayed statistics reasonably fresh:
export const ExtensionStats = {
CACHE_KEY: 'dotuniverse_ext_stats_v1',
CACHE_TTL: 24 * 60 * 60 * 1000, // 24 hours (1 day)
init() {
// 1. Instantly apply cached data to DOM (avoids visible flicker)
const cached = this.getCached();
if (cached && cached.data) {
this.applyToDOM(cached.data);
}
// 2. Fetch in background only when the 24-hour window expires
if (!cached || Date.now() - cached.timestamp > this.CACHE_TTL) {
this.refresh();
}
},
// ...
};
Clean DOM Mutation via Semantic Attributes
In index.html, each extension card includes a semantic data-ext-name identifier:
<div class="ext-card" data-ext-name="codetune">
<div class="ext-header">
<div class="ext-name"><span class="prefix">Code</span><span class="suffix">Tune</span></div>
<span class="ext-version">v1.3.0</span>
</div>
...
<div class="ext-stores">
<a ... class="store-badge vs">
<span class="store-info"><span class="store-name">VS Marketplace</span><span class="store-dl">737 installs</span></span>
</a>
<a ... class="store-badge ovsx">
<span class="store-info"><span class="store-name">Open VSX</span><span class="store-dl">1,500 downloads</span></span>
</a>
</div>
</div>
When fresh metrics arrive:
-
.ext-versionupdates to the store's current version (e.g.v1.3.0,v2.2.1,v3.5.1). - Store badges update with formatted counts (e.g.,
1,040 installs,6,000 downloads). - The grand total (
19,500+) is calculated and smoothly updated in both the header stats and the hero banner.
π€ Layer 3: Automated Daily CI/CD Synchronization
To keep the repository README.md and static HTML in sync for users without JavaScript or search bots, we created a zero-dependency Node script: scripts/sync-extension-stats.mjs.
Running this script fetches live data, parses index.html and README.md, updates markdown tables, Shields.io badges, and prints a formatted terminal overview:
$ node scripts/sync-extension-stats.mjs
π Fetching live extension stats from VS Marketplace and Open VSX...
π Live Extension Stats:
βββββββββββ¬βββββββββββββββ¬βββββββββββ¬βββββββββββββββββ¬βββββββββββ¬ββββββββββββββββββ
β (index) β Extension β Version β VS Marketplace β Open VSX β Total Downloads β
βββββββββββΌβββββββββββββββΌβββββββββββΌβββββββββββββββββΌβββββββββββΌββββββββββββββββββ€
β 0 β 'CodeTune' β 'v1.3.0' β '734' β '1,486' β '2,220' β
β 1 β 'dotcommand' β 'v2.0.1' β '778' β '970' β '1,748' β
β 2 β 'DotEnvy' β 'v2.2.1' β '758' β '2,173' β '2,931' β
β 3 β 'DotShare' β 'v3.5.1' β '1,040' β '5,987' β '7,027' β
β 4 β 'DotFetch' β 'v2.1.1' β '470' β '2,766' β '3,236' β
β 5 β 'DotReadme' β 'v1.2.0' β '364' β '1,719' β '2,083' β
β 6 β 'DotSense' β 'v1.3.0' β '257' β '0' β '257' β
βββββββββββ΄βββββββββββββββ΄βββββββββββ΄βββββββββββββββββ΄βββββββββββ΄ββββββββββββββββββ
π Grand Total Downloads: 19,502 (Marketplace: 4,401, Open VSX: 15,101)
β
index.html updated successfully.
β
README.md updated successfully.
To run this hands-free, we added .github/workflows/update-extension-stats.yml:
name: Auto Update Extension Stats
on:
schedule:
# Runs everyday at midnight UTC
- cron: '0 0 * * *'
workflow_dispatch: # Allows manual trigger from GitHub UI
permissions:
contents: write
jobs:
sync-stats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Sync extension downloads and versions
run: node scripts/sync-extension-stats.mjs
- name: Commit and push changes if updated
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add index.html README.md
if git diff --staged --quiet; then
echo "No changes detected."
else
git commit -m "chore: auto-update extension downloads and versions [skip ci]"
git push
fi
π‘ Clean Commit History: The
git diff --staged --quietcheck ensures that GitHub Actions only commits when marketplace metrics or versions actually change. This keeps the Git history clean from empty daily noise.
π The Bigger Picture: Building in Public
Automating tool statistics ties directly into how I build in public. Alongside extensions, dotUniverse features an open Growth Challenge Tracker across 9 creator and developer platforms.
Here is the current state for October 2026:
| Platform | Current (Now) | Target (Goal) | Remaining | Progress |
|---|---|---|---|---|
| π dev.to | 3,986 | 4,200 | +214 |
95% |
| πΌ LinkedIn | 709 | 800 | +91 |
89% |
| π΅ TikTok | 37 | 60 | +23 |
62% |
| βΆοΈ YouTube | 30 | 60 | +30 |
50% |
| βοΈ Medium | 6 | 10 | +4 |
60% |
| π Facebook | 15 | 20 | +5 |
75% |
| πΈ Instagram | 7 | 10 | +3 |
70% |
| π X / Twitter | 18 | 20 | +2 |
90% |
| π¦ Bluesky | 21 | 30 | +9 |
70% |
On dev.to specifically, we recently passed 15,400+ total views, 93 published posts, 97 reactions, and 67 comments.
Because dev.to is currently at 3,986 / 4,200, we explicitly commented out the celebratory 15-DAY RECORD badge in the HTML until the 4,200 mark is officially crossed. Milestones are meaningful only when they are genuine!
π‘ Key Engineering Takeaways
- Leverage Dual-Path Synchronization: Combining client-side background sync with scheduled CI/CD ensures that both web visitors and static repository viewers get consistently updated extension statistics.
- Explore Public API Capabilities: Both the Visual Studio Marketplace and Open VSX support CORS directly. Before introducing a custom serverless proxy, inspect the network headers first.
-
Respect the Client Cache: A 24-hour
localStorageTTL reduces visible flickering, avoids API-dependent initial rendering on repeat visits, respects third-party rate limits, and matches the realistic release frequency of developer tools. -
Conditional CI/CD Commits: Always check
git diff --staged --quietbefore committing automated bot changes to keep your repository history concise and meaningful.
π Project Links
- π Live Website: universe.dotsuite.dev
- π» GitHub Repository: kareem2099/dotuniverse
- π¦ VS Code Extensions: Search for
FreeRaveon VS Code Marketplace or Open VSX!
How do you automate metrics across your open-source repositories? Do you rely on static shields or live client-side queries? Let's discuss in the comments below!
Top comments (0)