I built a click counter. You click the button and it increments a number in a database. The updates are sychronized automatically for all users.
I wanted to build something that couldn't be fully built by ChatGPT with a single prompt. Lots of things that seem difficult are now trivially easy:
Setting up a button on a webpage that increments a value in a database is theoretically very simple the same way installing a garbage disposal in a sink is theoretically very simple. ChatGPT can explain how to do both quite well, but (at least for now) you'll still have to get in there and get your hands dirty and you'll probably bang your head a few times and make mistakes that add several hours to what you thought would be a 30 minute task.
The Goal
- Show a counter that increments when anyone clicks.
- Update instantly everywhere.
- Keep bots from hammering the DB and spiking the bill.
Why not the naive approach?
The lazy way would be: client calls increment()
directly on Firestore, with security rules that allow anyone to read/write, and there’s no App Check or throttling. That works for a demo, but bots/attackers could smash it with fake clicks, inflate counts, and run up usage. The free tier of Firestore allows for 20k writes per month. I don't expect random bots to click my button 20k times, but they could.
Architecture
- Frontend: Next.js, Firestore listener in the client.
- Database: Firestore doc
counters/global
. - Security: Firebase App Check + reCAPTCHA v3. reCAPTCHA v3 uses ✨algorithms✨ to give a score between 0 (bot) and 1 (human) estimating if the request (in this case the increment call to the Firestore db) is legit. reCAPTCHA v1 was the distorted text you had to type into a box. For this button I set it to allow anything over 0.5 since the stakes are pretty low. For e-commerce or logins etc I'd set a higher value.
- Serverless API: Next.js
/api/increment
route with Firebase Admin SDK. - Rate limiting: Per‑IP token bucket to stop spammers.
Firestore rules look like this:
allow read: if true;
allow write: if false;
All writes are server‑only.
Mistakes/Issues Encountered
1. Cross‑project credentials
My Firebase project (Firestore + App Check) was separate from the App Engine project hosting the site. That’s apparently common practice. Some teams separate resources for billing or access control. Problem: App Check tokens weren’t verifying because of this, so the count wasn't displaying and clicking the button resulted in an error. Fix: grant the App Engine default service account from the web project the roles datastore.user
and firebaseappcheck.verifier
in the Firebase project. No Service Account JSON. Took me lots of troubleshooting (and tons of added logs) to figure this out since I had assumed they were in the same Project.
2. App Check initialization order
Firestore was throwing permission errors when App Check enforcement was on. Turns out I was starting Firestore before App Check. Fix: initialize App Check first in the browser.
3. Stray credentials
After I got everything working locally I tried to deploy to "production" and my server blew up. Adding a bunch of logs I discovered it was looking for a JSON key at /Users/dustygalindo/Downloads/...json
. I had a GOOGLE_APPLICATION_CREDENTIALS
in .env.local
that pointed to a "Service Account" json file with credentials while trying a potential fix for the cross-project issue. Next.js shipped the variable with the json filepath into deploy, but App Engine couldn’t find that path. Removing it let App Engine fall back to its default service account (with the IAM roles mentioned above). Lesson: don’t carry local secrets into prod.
How It Works (Final Flow)
- Client boots Firebase + App Check with reCAPTCHA v3.
- Client listens to
counters/global
viaonSnapshot
. - User clicks → client fetches
/api/increment
. - API verifies App Check token, rate limits, increments Firestore.
- Firestore pushes the update out in real time.
Lessons Learned
- Cross‑project setups are fine—just wire IAM correctly.
- Initialize App Check before Firestore.
- Debug tokens save sanity in dev.
- Keep Firestore rules minimal when paired with App Check + API writes.
- Double‑check env vars; stray creds cause bizarre errors.
App Check isn’t just for clickers
App Check with reCAPTCHA isn’t only for silly counters. It’s useful anywhere you accept input—forms, logins, password resets. Bots are getting better, with LLM agents poking at weak spots. App Check should filter most bot spam before your backend even runs. That cuts waste, makes rate limiting effective, and protects your actual users.
It’s a bit of wiring and overkill for a button clicker on a site with only a few visits per year, but the same setup scales to real forms and auth flows.