Back to the work

Case notes · PWA + website

Zaitun

A complete app for my church choir: schedules, attendance, repertoire, and announcements in members' pockets, with a public website where the gallery lives and the board reads the attendance recap.

React · Firebase · Vercel · Live and in daily use · App screenshots show sample data

Why it exists

I sing in my church's choir, and the choir ran on the usual stack: a WhatsApp group where schedules scrolled away, attendance taken by asking around, sheet music as scattered files, and nobody quite sure which songs were performance-ready.

The thought was simple: what if the choir had an app that is actually complete? Not a group chat with extra steps, but one place for the schedule, an attendance system, the repertoire with scores attached, and announcements that stay put. And since the choir also needed a public face, a website for the gallery, where the board can see the one number they keep asking for: who is actually coming to rehearsal.

Two surfaces, one database. The app is a PWA that lives on a member's home screen; the website is where visitors meet the choir and admins read the recap.

The app in members' pockets

Zaitun member app home with next rehearsal, RSVP status, and announcements, sample data

One glance, whole choir

A member opens the app and sees what matters: the next rehearsal, whether they have confirmed attendance, and the announcements that used to drown in a group chat. The interface is in Bahasa Indonesia because that is the choir's language.

Zaitun schedule screen with rehearsals, services, and attendance counts, sample data

Schedules that collect their own attendance

Rehearsals and service days live on one monthly list, and every event carries its own RSVP: attending, excused, or silent. An admin can share a per-event link into WhatsApp; a member who taps it lands directly on that event with the confirmation sheet already open.

Attendance is written as one document per member per event, keyed by both ids, so answering twice can only ever update your answer, never double-count it.

Zaitun repertoire screen with song statuses and sheet music links, sample data

The repertoire, with the sheet music attached

Every song the choir sings, tracked from new to learned to mastered, with the score one tap away as a PDF. Scores can be scoped to voice sections, so an alto sees the alto arrangement and nobody prints the wrong part again.

Zaitun profile screen with attendance stats and pending member approvals, sample data

Members let themselves in, admins hold the door

Anyone can sign in with Google, but a new account waits in a pending state until an admin approves it from this screen. The choir manages its own roster: voice type, role, and membership, with no developer in the loop.

The public face

The website is what the congregation and future members see: who the choir is, the gallery of moments, and how to join. It shares one database with the app, so a schedule created for members is the same schedule the public site can show.

Zaitun public website gallery page with photo albums
The public site's gallery, fed from the same database as the app.

Attendance, answered

The question the choir board actually asks is not who tapped a button, it is who keeps showing up. The recap page turns the app's RSVPs into per-rehearsal and per-member percentages, sorted so the members who need a check-in float to the top.

Before this page, that answer meant reconciling a paper list against memory. Now it is a month picker.

Zaitun admin attendance recap with per-rehearsal and per-member percentages, sample data
The recap: attendance per rehearsal and per member, computed from RSVPs.

The hard parts

Two apps, one truth

The member app and the website are separate deployments sharing one Firestore database. A schedule created in the app appears in the web admin; a member approved on the web can sign in on the phone. The discipline is in the collection contracts: both apps read and write the same shapes, documented once, and neither invents its own.

Attendance that cannot double-count

An RSVP document's id is the schedule id joined with the member id. That single choice makes attendance idempotent: confirm, change your mind, confirm again, and there is still exactly one record of you for that event. The recap page can then just count documents, with no deduplication step to get wrong.

Queries a free tier can love

Every schedule stores its year and month as a plain field, so the monthly views on both apps are a single equality query with no composite index. Small denormalizations like that, plus a photo count cached on each album, keep the whole system inside Firestore's free tier while feeling instant.

Notes on the build

Two Vite and React apps in one repository, deployed separately on Vercel so a change to the app never rebuilds the website and vice versa. Firebase provides Google sign-in and Firestore; membership approval, roles, and voice types are all managed inside the app by the choir's own admins.

If you want to talk about this build, or something you want built, say hello on WhatsApp or write to philip.kamdani@gmail.com.

Back to the work