Skip to the work

Donation receipts for Ganesh mandals - my own

Pavti Pustak

2026

1 / 5

Overview

Pavti Pustak - the receipt book, without the paper. During Ganeshotsav volunteers go door to door collecting contributions and write each one in a book; every year that book went the way paper goes - and it took the records with it, who gave and how much and the contacts you wanted again next year, with somebody still adding it all up by hand. Now it is a phone: the volunteer records the contribution, the donor has a WhatsApp receipt before the door shuts, the treasurer has the totals already added, and every year of it stays a tap away instead of in a drawer. Mine end to end - idea, design, database, code, deployment - and it ran a real mandal’s collection.

Stack

App

  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind v4
  • Server Actions
  • Zod

Data

  • Supabase
  • PostgreSQL
  • Row Level Security
  • Realtime
  • Triggers
  • Partial indexes

Offline

  • IndexedDB
  • PWA
  • Service worker

Quality

  • Vitest
  • ESLint
  • Speed Insights
  • Core Web Vitals
  • Vercel

What I built

What it does

  1. 01Runs at zero cost, and that is a design constraint rather than an accident - free tiers all the way down, with a scheduled ping so the database never pauses for inactivity. A neighbourhood mandal has no budget for software.
  2. 02Works with no signal, which matters in a crowded lane during the festival. Receipts are queued in IndexedDB - a small layer I wrote rather than a library - and the screen shows what the volunteer believes is true, not what has synced yet.
  3. 03Every volunteer’s phone stays in step through Supabase Realtime - a receipt entered on one device is on everybody else’s screen inside a second, so two people collecting on the same street never double-count or overwrite each other.
  4. 04One tap sends the donor a bilingual thank-you over WhatsApp, which is the receipt they actually keep.
  5. 05Tracks pledges as well as cash: money promised for a date is not money collected, so the dashboard reminds you on the day and never counts it early.
  6. 06The treasurer prints a ruled PDF ledger for a day, a date range or the whole festival - laid out with print CSS so it needs no library, and split so a part-paid contribution appears under both received and outstanding. The column has to add up when somebody checks it by hand.
  7. 07An activity log covering every ledger - receipts, expenses, donations - written by Postgres triggers rather than by the app, so nothing can be changed without a record. Each entry shows who made it, when, and a field-by-field diff of what moved. People are handling other people’s money and the book has to be answerable.
  8. 08Marathi first, English second, because the people using it are not reading English.

How it is built

  1. 09The database does not trust the app. Row Level Security is on every table and the write path is closed by 21 policies, because the anon key is public by design and every volunteer holds a real JWT - so PostgREST can be called directly, with or without my UI. Attribution and slip numbers are set by Postgres on insert and frozen on update, so a volunteer cannot reassign somebody else’s receipt or duplicate a number; a unique index makes the collision impossible rather than unlikely.
  2. 10Every input is parsed by a Zod schema before it reaches a Server Action, and then checked again by a CHECK constraint in the database. The schema is for the volunteer, who needs to be told what is wrong with the amount they typed; the constraint is for everybody who skips the form.
  3. 11Written for latency on a mid-range phone on festival-night mobile data. Reads are server-rendered and land as HTML rather than as a spinner waiting on a fetch, filters move in a transition so the list never blocks the tap, and thirteen indexes - partial ones where the query is partial, like the unpaid-and-due list - keep the dashboard’s totals a single scan instead of a table sweep. Core Web Vitals are measured in production through Speed Insights, with receipt ids stripped from the URL before it leaves the browser, since an id is a handle to a named contributor’s record and the vitals report does not need it.
  4. 12291 tests over 21 files, run in Vitest with TypeScript strict and ESLint behind one verify script. The money arithmetic is where they are concentrated - partial payments, pledge dates, the split between received and outstanding - because those are the numbers somebody checks by hand against a printed column.
  5. 13The interface is built to be used one-handed, in a crowd, at night. shadcn/ui on Base UI primitives so every dialog and menu is keyboard reachable and screen-reader labelled without my reinventing it, optimistic toggles that respond on the tap rather than after the round trip, skeleton states on the five slowest routes so a slow network degrades into a shape rather than a blank screen, and dark mode because a volunteer reading a phone in a lit street at 10pm should not be handed a white page.

Links

The live app runs a real mandal’s collection, so the demo above is the same app with invented data. A scaled version is next, and that one will need paid infrastructure.