Skip to content

Commit 4c15e00

Browse files
Document setup process for visit counter
Added detailed setup instructions for self-hosted visit counter using Cloudflare Workers and KV.
1 parent ce2f15f commit 4c15e00

1 file changed

Lines changed: 202 additions & 0 deletions

File tree

worker/SETUP.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Self-hosted visit counter — setup
2+
3+
Cloudflare Worker + KV. ~10 minutes, no credit card.
4+
5+
## 1. Cloudflare account
6+
7+
1. Go to <https://dash.cloudflare.com/sign-up>, sign up with your email, verify it.
8+
2. You do **not** need to add a domain. Workers run on `*.workers.dev` for free.
9+
10+
## 2. Get the code onto your machine
11+
12+
The worker lives in this repo under `worker/`:
13+
14+
```bash
15+
git clone https://github.com/RealMathModel/RealMathModel.github.io.git
16+
cd RealMathModel.github.io/worker
17+
```
18+
19+
```
20+
worker/
21+
src/index.js
22+
wrangler.toml
23+
```
24+
25+
(GitHub Pages will serve these two files as static text at
26+
`/worker/src/index.js` etc. That is harmless — they contain no secrets — but
27+
if you would rather they not be public, move the `worker/` folder to its own
28+
private repo.)
29+
30+
You need Node 18+ (`node -v`). `npx` ships with it; there's nothing to install
31+
globally.
32+
33+
## 3. Log in
34+
35+
```bash
36+
cd worker
37+
npx wrangler login
38+
```
39+
40+
A browser window opens; approve the access request.
41+
42+
## 4. Create the KV namespace
43+
44+
```bash
45+
npx wrangler kv namespace create HITS
46+
```
47+
48+
It prints something like:
49+
50+
```
51+
[[kv_namespaces]]
52+
binding = "HITS"
53+
id = "a1b2c3d4e5f60718293a4b5c6d7e8f90"
54+
```
55+
56+
Copy that `id` into `wrangler.toml`, replacing `PASTE_YOUR_KV_NAMESPACE_ID_HERE`.
57+
58+
Optional — start the count at a non-zero number (e.g. carry over your
59+
GoatCounter total):
60+
61+
```bash
62+
npx wrangler kv key put --binding=HITS total "48213" --remote
63+
```
64+
65+
## 5. Deploy
66+
67+
```bash
68+
npx wrangler deploy
69+
```
70+
71+
Output ends with the live URL, e.g.
72+
`https://rmm-counter.yourname.workers.dev`.
73+
74+
Check it:
75+
76+
```bash
77+
curl -s https://rmm-counter.yourname.workers.dev/
78+
# {"count":0}
79+
```
80+
81+
(That `curl` reads without incrementing — only `POST` from the site's origin
82+
increments.)
83+
84+
## 6. Wire up the page
85+
86+
In `index.html`, replace the GoatCounter badge IIFE with the block in
87+
`counter-block.html`, and set:
88+
89+
```js
90+
var API='https://rmm-counter.yourname.workers.dev/';
91+
```
92+
93+
Commit and push. Load the site, open DevTools → Network, confirm a `POST` to
94+
the worker returning `{"count":N}` and the badge appearing.
95+
96+
## 7. Later changes
97+
98+
Edit `src/index.js`, run `npx wrangler deploy` again. To read or reset the
99+
count:
100+
101+
```bash
102+
npx wrangler kv key get --binding=HITS total --remote
103+
npx wrangler kv key put --binding=HITS total "0" --remote
104+
```
105+
106+
---
107+
108+
## What the number means
109+
110+
**Counts:**
111+
112+
- Every full page load from a browser that runs JS and isn't blocking the
113+
worker domain — once per tab session (the `sessionStorage` guard).
114+
- Repeat visitors: each new tab, and each new session in the same tab after
115+
the browser clears session storage. This is closer to "visits" than to
116+
"pageviews" or "unique visitors".
117+
118+
**Doesn't count:**
119+
120+
- **Bots and crawlers** — Googlebot, GPTBot, most scrapers: no JS execution,
121+
no fetch, no count. Headless browsers *will* count. This is a feature: your
122+
number stays closer to human traffic than a server log would.
123+
- **Ad blockers / privacy extensions** — uBlock Origin et al. block by
124+
hostname lists. `workers.dev` isn't on those lists today, so you'll lose far
125+
fewer hits than GoatCounter does. But a strict blocker or a `connect-src`
126+
CSP could still drop it, and then the badge just stays hidden.
127+
- **Prefetches / speculative loads** — Chrome's prerender runs JS, so a
128+
prerendered page that the user never visits *can* count. Small inflation,
129+
rare in practice.
130+
- **Hash-route navigation** — deliberately excluded by the session guard.
131+
Clicking around inside the Study Hub adds nothing.
132+
- **Users with JS off**, and anyone whose fetch fails for any reason.
133+
134+
Net: treat the number as "browser sessions that got far enough to run the
135+
script". It will read **lower** than GoatCounter's pageview total (which counts
136+
every hash route change) and **higher** than its unique-visitor count. Keep
137+
GoatCounter open for referrers and per-page breakdowns — the beacon is
138+
untouched.
139+
140+
## Free-tier limits
141+
142+
Workers free plan, per day, per account:
143+
144+
| Resource | Free allowance |
145+
|---|---|
146+
| Worker requests | 100,000 / day |
147+
| CPU time | 10 ms per request (this worker uses well under 1 ms) |
148+
| KV reads | 100,000 / day |
149+
| KV writes | **1,000 / day** |
150+
| KV storage | 1 GB |
151+
152+
**The binding constraint is KV writes: 1,000/day.** One write = one counted
153+
visit. So you're fine up to roughly **1,000 new visits per day**; reads
154+
(repeat loads in an already-counted tab) are effectively free at this scale.
155+
156+
Past that, writes start failing — the worker still returns the last known
157+
count, so the badge keeps working, it just stops rising until midnight UTC.
158+
If you ever get there, the fix is the Workers Paid plan ($5/mo) or the
159+
alternative below.
160+
161+
There is also a hard KV limit worth knowing: **~1 write per second to the same
162+
key**. Sustained bursts above that get rate-limited.
163+
164+
## The honest caveat about KV (read this)
165+
166+
You asked for Worker + KV, and for a personal site it's fine. But KV is the
167+
wrong *primitive* for a counter, and I'd rather you know why:
168+
169+
1. **Lost updates.** Read-modify-write isn't atomic. Two visitors landing
170+
within the same few hundred milliseconds both read `N` and both write
171+
`N+1`. You lose one. At your traffic this is a rounding error; at 10
172+
concurrent visitors it's a visible undercount.
173+
2. **Eventual consistency.** KV reads are served from edge caches and can lag
174+
writes by up to ~60 seconds globally. A visitor in Cairo may see a number
175+
a minute behind one in Frankfurt. You specifically asked for "live, not
176+
hourly-cached" — KV gives you "usually within a minute", not "exact".
177+
3. **1,000 writes/day** as above.
178+
179+
**Durable Objects solve all three.** A Durable Object is a single-threaded
180+
addressable instance with its own transactional storage — increments are
181+
genuinely atomic, reads are strongly consistent, and it's on the Workers free
182+
tier now (with a much higher write budget). It's about the same amount of code
183+
and the same `wrangler deploy`.
184+
185+
If you want it, say so and I'll ship the DO version — same API shape, same
186+
front-end block, so nothing in `index.html` changes. I built KV because you
187+
specified it, and it will work; I just wouldn't call it the better design.
188+
189+
## Security notes
190+
191+
- No token, key, or secret is in `index.html`. The page sends an unauthenticated
192+
`POST` and reads a number back. That's the whole contract.
193+
- `Access-Control-Allow-Origin` is the exact site origin, never `*`, and
194+
`Vary: Origin` is set so no cache mixes responses across origins.
195+
- CORS is a *browser* control, not a security boundary. Anyone can `curl -X
196+
POST` your worker with a forged `Origin` header and inflate the count. For a
197+
visit badge that's acceptable; there's no way to prevent it without either a
198+
secret in the page (which you correctly ruled out) or Cloudflare Turnstile.
199+
If it ever gets abused, add a Cloudflare Rate Limiting rule on the worker
200+
route — it's free and takes two minutes.
201+
- `Cache-Control: no-store` on every response keeps browsers and Cloudflare's
202+
edge from serving a stale number.

0 commit comments

Comments
 (0)