Your own GitHub profile views counter. Free forever. Can't break.
A drop-in replacement for komarev.com/ghpvc — except you own the URL, you own the data, and there's no third-party service that can go down on a random Tuesday.
🌐 Live demo — counter.othmaro.dev
Built by @othmarodev — othmaro.dev
If your GitHub profile README has ever displayed a Profile views: ? broken
image — that's because the free third-party counter you were using went down.
Or got rate-limited. Or moved server. Or just disappeared.
It's not their fault. They're free services run by individuals, often hosted in a single region, with no SLA. When they break, your counter resets to zero. Your historical count — months or years of pride — is gone.
profile-counter is the same idea, but you own the deployment:
- Runs on your Vercel project (free tier, never charged)
- Stores count in your Upstash Redis (free tier, never charged)
- Exposes a URL you control:
your-project.vercel.app/api/views - Migrate from komarev/laobi by seeding the counter (no zero reset)
- 100% open source — fork it, audit it, modify the badge style, do whatever
If it ever breaks, it's because you broke it. And you can fix it.
That badge above is rendered by this exact code, deployed to counter.othmaro.dev — go play with the demo (style examples, color presets, the live counter). Every time you reload this README, the badge above counts up.
Before you start, make sure you have:
- A GitHub account (free) — github.com if not
- A Vercel account (free) — sign up at vercel.com/signup with your GitHub
- An Upstash account (free) — we'll create this in step 1
- ~10 minutes of focus
For the optional "migrate from komarev" step (Step 4), you'll also need:
- Node.js 18+ installed locally (check with
node --version— if you seev18or higher, you're good) - A terminal (Terminal.app on Mac, PowerShell on Windows, any shell on Linux)
- Git (check with
git --version)
If you skip the migration step, you don't need anything local — the whole setup happens in the Vercel and Upstash dashboards in your browser.
In a hurry? Click the Deploy with Vercel button below, sign up to Upstash on the side, paste the two values from Upstash into Vercel when prompted. Done in 5 minutes. The walkthrough below has every click spelled out for first-timers.
Upstash is a managed Redis service. Free tier gives you 10,000 commands/day — enough for ~5,000 profile visits per day, far more than any individual profile gets.
-
Go to console.upstash.com and click Sign Up
-
Pick "Continue with GitHub" (no credit card asked, ever)
-
Once logged in, click "Create Database" (big button, top right)
-
Fill the form:
- Name:
profile-counter(or any name you like) - Type:
Regional(cheapest, free) - Region: pick the one closest to where most of your readers are.
us-east-1(N. Virginia) is a safe default — it's also Vercel's default region - Eviction: leave the default (
no eviction)
- Name:
-
Click "Create" at the bottom
-
The database opens. Look for the "Connect" section (or the "REST API" tab in older UIs). You'll see two values you need to copy:
UPSTASH_REDIS_REST_URL="https://your-database-name.upstash.io" UPSTASH_REDIS_REST_TOKEN="AcXyAIjc...long-secret..." -
Click the eye icon 👁 next to the token to reveal it. Copy both lines to a temporary note (TextEdit / Notes app / wherever).
⚠️ Treat the token like a password — never commit it, never paste it in chats, never put it in a public gist. We'll only paste it into Vercel in the next step.
✅ What success looks like: You have a temporary note open with both UPSTASH_REDIS_REST_URL=... and UPSTASH_REDIS_REST_TOKEN=....
Click the button. Vercel will clone this repo into your account and ask for the two values you just copied:
The button takes you through this flow:
- Create a Git repo — Vercel will create
profile-counterin your GitHub account. Click "Create". - Configure environment variables — Vercel shows two empty fields:
UPSTASH_REDIS_REST_URL→ paste your URLUPSTASH_REDIS_REST_TOKEN→ paste your token
- Click "Deploy". Vercel builds the project (~30 seconds), then takes you to the project dashboard.
- Disable deployment protection (important — if you skip this, your badge will show "401 Unauthorized" to readers):
- In the project dashboard, go to Settings → Deployment Protection
- Find the section "Vercel Authentication" and turn the "Require Log In" toggle OFF
- Click Save if asked
✅ What success looks like: Your project's dashboard shows a green "Ready" badge and a public URL like https://profile-counter-yourname.vercel.app.
Prefer the command line? Click here for the CLI version.
# 1. Clone the repo
git clone https://github.com/othmarodev/profile-counter.git
cd profile-counter
# 2. Install the Vercel CLI (one-time, global)
npm install -g vercel
# 3. Log in
vercel login
# 4. Link / deploy
vercel
# Answer the prompts:
# - Set up and deploy? Y
# - Scope? (your username)
# - Link to existing project? N
# - Project name? profile-counter
# - Code dir? ./
# - Modify settings? N
# 5. Add env vars from your terminal (you'll paste each value into the prompt — never written to your shell history)
vercel env add UPSTASH_REDIS_REST_URL production
vercel env add UPSTASH_REDIS_REST_TOKEN production
# 6. Deploy to production
vercel --prodDon't forget to disable Vercel Authentication in the project's Settings (Step 2.4 above) — the CLI doesn't toggle it for you.
Before you put the badge in your README, confirm the counter is working.
Option A — In your browser
Open this URL (replace your-project with the Vercel URL you got in step 2):
https://your-project.vercel.app/api/views?id=test
You should see a badge image like this in the browser:
Reload the page a few times. The number should increase each time. (It may take 1–2 seconds the first time as Vercel cold-starts.)
Option B — With curl
curl -i "https://your-project.vercel.app/api/views?id=test"You want to see:
HTTP/2 200
content-type: image/svg+xml; charset=utf-8
...
<svg xmlns="http://www.w3.org/2000/svg" ... aria-label="Profile views: 1">
The aria-label="Profile views: 1" confirms storage works. Curl it again — should say : 2.
Seeing
HTTP/2 401or "Authentication Required"? You skipped step 2.4 — go back and turn off Vercel Authentication in Deployment Protection.
Seeing
aria-label="Profile views: ?"with a?? The env vars are missing or wrong. Go to your project → Settings → Environment Variables and double-check both values match what Upstash gave you. After fixing, click "Redeploy" on the latest deployment.
If you had a view count on komarev (or another service) you want to preserve, seed your new counter to that number. Otherwise skip — your counter starts at 1 on the next visit.
You need Node.js installed locally for this step.
# 1. Clone the repo (if you used the button in step 2, you can clone YOUR fork)
git clone https://github.com/YOUR_USERNAME/profile-counter.git
cd profile-counter
# 2. Install dependencies
npm install
# 3. Create a local .env from the template
cp .env.example .env
# 4. Edit .env with your Upstash credentials
# On Mac/Linux:
nano .env
# On Windows:
notepad .env
# Paste the same UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN
# you used in Vercel. Save and close.
# 5. Seed the counter for your GitHub username
node scripts/seed.js --id=YOUR_USERNAME 623
# Output:
# ✓ Seeded "profile-counter:YOUR_USERNAME" to 623Replace YOUR_USERNAME and 623 with your values. The next time someone views your README, it'll show 624.
Need more options? Run
node scripts/seed.js --helpfor the full command reference (read mode, multiple buckets, etc.).
Don't know your old count? Check Wayback Machine for your profile — find a recent snapshot, read the number off the old badge. Or just pick a number that feels honest. It's your counter.
The
.envfile is gitignored — it won't accidentally get committed if you push your fork.
Replace your old komarev/laobi badge with your new URL:
Commit, push, then open your GitHub profile. The first load triggers GitHub's image proxy (camo) to cache the badge — give it ~30 seconds. Subsequent visits show the counter incrementing.
✅ That's it. You own this counter forever.
All options are URL query parameters — no rebuild needed.
| Param | Default | Description |
|---|---|---|
id |
default |
Bucket key. Use different ids to track different pages from one deployment (e.g., ?id=othmarodev, ?id=my-project). |
label |
Profile views |
Text on the left side. URL-encode spaces. Max 40 chars. |
leftColor |
#555 |
Left background. Hex or named (blue, red, blueviolet, ...). |
rightColor |
#4c1 |
Right background. Same accepted formats. |
style |
flat |
flat (shields.io-style with gradient) or flat-square. |
<!-- Default -->
<!-- Custom label + blueviolet -->
<!-- Brand colors (dark + orange) -->
<!-- flat-square style -->
Use distinct ids. Each id is an independent counter:
<!-- Your GitHub profile -->

<!-- A project README -->

<!-- A blog post -->
All three count separately. One deployment, infinite buckets.
┌──────────────────────┐ HTTPS ┌──────────────────────┐
│ Reader's browser │ ──────────▶ │ camo.githubusercontent│
│ loading your README │ │ .com proxy │
└──────────────────────┘ └─────────┬────────────┘
│ on cache miss
▼
┌──────────────────────────┐
│ your Vercel edge function│
│ /api/views │
└─────────┬────────────────┘
│ INCR
▼
┌──────────────────────────┐
│ Upstash Redis │
│ profile-counter:{id} │
└──────────────────────────┘
- Edge runtime for low latency (Vercel routes the request to the closest region — usually 30-80ms for the increment + render).
- Atomic INCR so concurrent requests can't lose increments.
- No GitHub API calls — the counter is purely your storage.
- camo cache is the one thing you can't control — GitHub re-proxies the badge image only when the cache TTL expires (usually a few minutes), so the number you see is monotonically increasing but not "every-pageview-exact". This is the same limitation that komarev, hits.sh, visitor-badge.laobi.icu and every other counter on the planet has.
| profile-counter | komarev.com | visitor-badge.laobi.icu | hits.sh | |
|---|---|---|---|---|
| You own the URL | ✅ | ❌ | ❌ | ❌ |
| You own the data | ✅ | ❌ | ❌ | ❌ |
| Can't be turned off by a third party | ✅ | ❌ went down 2024, 2025, 2026 | ❌ | ❌ |
| Migrate from komarev (seed initial count) | ✅ | n/a | ❌ | ❌ |
| Open source | ✅ MIT | ❌ | ❌ | ❌ |
| Multiple counters per deployment | ✅ | ❌ | ❌ | ❌ |
| Free | ✅ Vercel + Upstash free tiers | ✅ | ✅ | ✅ |
| Setup time | 5 min | 0 min | 0 min | 0 min |
The trade-off is honest: free third-party services beat self-hosted on "time to first badge". Self-hosted wins on every other axis.
The eight most common issues people hit. Click to expand.
My badge shows "Authentication Required" / a 401 error
Vercel's "Deployment Protection" is on for your project. Disable it:
- Open your project on vercel.com → Settings → Deployment Protection
- Find "Vercel Authentication" and turn the "Require Log In" toggle OFF
- Click Save
- Reload your badge — it should work within a few seconds
My badge shows "Profile views: ?" (a literal question mark)
This means the Edge function ran but couldn't reach your Redis. Check:
- Both
UPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKENare set in your Vercel project → Settings → Environment Variables - The values match exactly what Upstash shows in the database "Connect" tab (no extra spaces, no quotes around the values)
- After fixing, go to Deployments, find the latest one, click the
...menu → Redeploy (without rebuild is fine)
"vercel: command not found" when I try the CLI
The Vercel CLI isn't installed. Run:
npm install -g vercelIf you get permission denied on Mac/Linux, use:
sudo npm install -g vercelOr use a Node version manager like nvm so you don't need sudo:
github.com/nvm-sh/nvm.
"node: command not found" / npm not installed
You don't have Node.js. You only need it for the optional seed step (Step 4). Install from nodejs.org — pick the LTS version. Verify after install:
node --version # should print v18.x or higher
npm --version # should print 9.x or higherThe counter doesn't increase when I reload my README
This is GitHub's image cache (camo), not a bug in the counter.
GitHub proxies external images through camo.githubusercontent.com and caches them for several minutes. Your function is incrementing, but camo serves you the same cached SVG until the TTL expires.
To verify the counter actually moves:
curl -s "https://your-project.vercel.app/api/views?id=YOUR_USERNAME" | grep aria-label
# Run this 3 times — the number should go up each time.If curl shows the count moving but the badge in your README doesn't, that's camo cache — wait a few minutes.
I changed the env vars but the badge still shows old behavior
Vercel applies env var changes only to new deployments. After changing variables:
- Go to Deployments in your project
- Click the
...menu on the latest deployment → Redeploy - (You can uncheck "Use existing build cache" for a totally clean rebuild, but it's usually not necessary)
Wait ~30 seconds for the new deployment to go live, then reload your badge.
The seed script says "Missing UPSTASH_REDIS_REST_URL..."
The local .env file isn't set up. Make sure you:
- Are in the project root:
cd profile-counter - Created the file:
cp .env.example .env - Edited
.envwith your actual values (not the placeholders) - The two lines look like:
UPSTASH_REDIS_REST_URL=https://your-database-name.upstash.io UPSTASH_REDIS_REST_TOKEN=AcXyAIjc... - No surrounding quotes are needed — the seed script handles both with and without
Upstash signup blocked / not available in my country
Upstash is available globally but a handful of countries are restricted by US export controls. Workarounds:
- Try Vercel KV instead — same Redis API, same free tier
- Or use Redis Cloud's free tier (30 MB free, HTTP REST available with their REST proxy)
- Both swaps are ~2 lines in
api/views.js— the function only usesredis.incr(key)
Still stuck? Open an issue
If your problem isn't here, open an issue with:
- Your Vercel project URL
- The output of
curl -i "your-project.vercel.app/api/views?id=test"(full HTTP response, including headers) - Anything you tried already
I'll add the answer to this list so the next person finds it faster.
Why Vercel and not Cloudflare Workers?
Both work. Vercel was picked because most devs already have a Vercel account and the Deploy with Vercel button gets you to a working counter in one click. If you prefer Cloudflare, the Edge function adapts in ~20 lines — see Issue #1 (PR welcome).
Why Upstash and not [other Redis]?
Upstash Redis is HTTP-based, which is required for Vercel Edge functions (no TCP at the edge). Their free tier (10k commands/day) is generous and they don't ask for a credit card. If you already use a different HTTP-callable KV (Cloudflare KV, Redis Cloud HTTP API), swapping is one import change away.
Can I run this with no Redis (in-memory only)?
Vercel Edge functions are stateless — every invocation is a fresh isolate, so in-memory state is lost between requests. You need some external storage. Upstash is the lowest-friction option (free, no CC, no servers to manage), but the storage call is one line so you can swap it for anything.
Is the count accurate?
It's accurate up to GitHub's camo image cache. GitHub re-proxies the badge when its cache expires (usually a few minutes), and only THEN does our function get called and increment the count. Two visits within camo's cache window register as one increment.
This is the same limitation every counter has, including komarev. Counting exact pageviews would require GitHub to embed something other than a cached image, which isn't a thing.
Will this stay free?
Vercel Hobby plan: free, with 100k function invocations per month. A wildly popular profile gets ~1,000 daily views = ~30k/month. You'd have to be in the GitHub top-1000 to outgrow the free tier.
Upstash free: 10k commands/day. Each visit = 1 INCR = 1 command, so 10k daily visits. Same story.
If you somehow exceed both: the paid tiers are cheap (~$1/month) or you can swap to Cloudflare Workers (100k/day requests free).
Why is the badge always shown as `flat` style and not the GitHub-native one?
GitHub README image badges are static images — the platform doesn't have a
"native" badge concept. What you're used to seeing is the
shields.io flat style, which is what we mimic in
api/views.js. The flat-square variant is also supported (?style=flat-square).
Can I use this for non-GitHub pages?
Yes. It's just an <img> tag serving an SVG. Put it in your personal site, in
a blog post, in a project README hosted on GitLab, anywhere. Use different
?id= values to track separate pages.
PRs welcome. The codebase is intentionally tiny — under 300 lines of JS total. Good first PRs:
- Cloudflare Workers adapter (
api/views.cloudflare.js) - Additional badge styles (
plastic,social,for-the-badge) - Storage adapters (Vercel KV, Redis Cloud, MongoDB Atlas, Postgres)
- A
top.txtrecipe for the shields.io endpoint format, so users who prefer can route through shields.io for extra styles
- Edge function with Upstash Redis
-
flatandflat-squarestyles - Migration script from third-party counters
- Multiple counters per deployment via
?id= -
for-the-badgeandplasticstyles - Cloudflare Workers adapter
- Privacy-friendly analytics endpoint (
/api/stats?id=..., requires auth) - Webhook on milestone counts (Discord/Slack)
MIT © 2026 Othmaro Fallas Rojas
If you deploy this and your counter works, consider a star — it's the GitHub-native equivalent of the badge.