← Back to Blog

Project 07 — Silent Auction

A silent auction and tombola platform for community fundraising, built on Google Sheets and Apps Script — bidder registration, live bidding, a sponsor-video tombola with server-side anti-cheat, and WhatsApp winner notifications, at zero monthly cost.

The Problem

Small community and charity events run silent auctions and tombolas on paper bid sheets and raffle tickets — slow, easy to dispute, and hard to promote sponsors through. The brief was a platform that a non-technical organiser could run end-to-end: bidder registration, live item bidding, a sponsor-video tombola, and winner notifications, without a server bill or a login system to maintain.

Google Sheets as the Database

The stack is the same free-tier pattern as the Incident Tracker project — static HTML/JS pages, a Google Apps Script web app as the REST API, and Google Sheets as the database, with nine tabs covering items, bids, bidders, config, and the equivalent tombola tables. doGet() and doPost() dispatch to action handlers, and a one-time setupSheets() call creates the entire schema — headers, tabs, and seed config — so standing up a new event doesn’t require touching a spreadsheet by hand.

Tombola Anti-Cheat: Tokens, Timing, and a Server-Side Lock

The tombola gives out raffle tickets for watching sponsor videos, which only works if a ticket can’t be self-awarded from the browser console. Watching a video issues a short-lived pending token; claiming a ticket checks that token server-side against elapsed time, not just its presence:

// Validate timing
const now       = new Date();
const issued    = new Date(issuedAt);
const elapsed   = (now - issued) / 1000;
if (elapsed < 28)           return { error: 'Video not completed — please watch the full ad' };
if (elapsed > tokenExpiry)  return { error: 'Token expired — please watch a new ad' };

A 28-second floor against a ~30-second video means the client can’t skip ahead and claim early, and an expiry window means a token can’t be held and replayed later. Because two bidders could claim from the same prize pool in the same second, the whole claim runs inside a LockService lock so ticket counts can’t be double-spent under concurrent requests:

// Use LockService to prevent concurrent claims
const lock = LockService.getScriptLock();
try {
  lock.tryLock(8000);
} catch(e) {
  return { error: 'Server busy — please try again in a moment' };
}
try {
  // ...validate token, bidder and prize, then award the ticket...
} finally {
  lock.releaseLock();
}

Everything between the lock and its finally — token lookup, prize capacity check, ticket write — runs as one atomic unit against the sheet.

Bidding and Registration

Registration issues a BID-XXXXXX bidder ID, with phone-based duplicate detection so a returning bidder gets their existing ID back instead of a second one, persisted client-side via localStorage. Each auction item enforces its own minimum bid increment, tracks the current leader by name and bidder ID, and a live countdown pulls the auction end time straight from the Config sheet rather than being hardcoded per event.

The Admin Dashboard and WhatsApp Loop

The organiser’s dashboard covers items (with drag-and-drop image upload through the ImgBB API), live bid tracking, and a winners view that’s auto-generated on “Close All.” Every notification — auction winners, tombola winners, sharing an item or the current top three — goes out as a pre-filled WhatsApp deep link rather than email or SMS, which matches how these events actually communicate on the day. Message formatting uses WhatsApp’s own markdown (*bold*, _italic_) instead of emoji so it renders consistently on both Android and Windows Chrome.

A Sponsor Proposal Generator

Beyond the auction itself, proposal-generator.html is a standalone tool: fill in event and sponsor details, get a live-updating, print-ready sponsorship proposal with cost-per-view calculated automatically from estimated attendance — exported via the browser’s own Print → Save as PDF, no PDF library required.

What This Project Demonstrates

  • Server-side anti-cheat — timing validation and single-use tokens instead of trusting anything the client reports
  • LockService concurrency control around a shared, limited resource (ticket counts) to prevent double-claims under simultaneous requests
  • A REST dispatcher pattern in Apps Script (doGet/doPost → action handlers) covering 22 endpoints across two related but independent systems — auction and tombola
  • Designing for the communication channel people actually use at these events — WhatsApp deep links, not email
  • A second, fully separate tool (the proposal generator) built to solve an adjacent real-world problem — getting sponsors on board in the first place

Live: silentauction.morneydeetlefs.workers.dev GitHub: silentAuction


Next: Project 08 — KlasWerk. A full-stack learning management platform for trainers and students — course authoring, lesson delivery, quizzes and PayFast payments. In active development.