Case study

GMSM-USA.

A community platform for people who don't have email addresses

Volunteer project · Sole developer · April 2026 · Live · in daily use

GMSM America is a nationwide US religious and cultural community of several hundred families that ran on spreadsheets and WhatsApp threads. Registering for an event meant an organizer scrolling back through chat messages to reconcile names, lodging needs, and attendance dates by hand, and there was no directory to look up who lived nearby. I built the platform that replaced all of it, solo, as volunteer work for the community — 340 members across 45 states, 463 registrations across 17 events so far.

340
Active members
Across 45 US states and 93 cities
0 → 280
First eight weeks
342 member records by August 2026
463
Event registrations
Across 17 events, including a 14-city national tour
79 → 293
Households to people
86% of member records, behind just 136 login accounts

The problem

Several hundred families spread across the United States were coordinating entirely through spreadsheets and WhatsApp threads. Event registration meant one organizer reading back through a chat log and reconciling names, lodging needs, and attendance dates by hand, message by message.

There was no member directory, so there was no way to answer a question as basic as who lives near me. And because signups lived in chat history, nothing survived the event — every gathering started the reconciliation over from scratch, with no record of who had signed up for what the last time.

Constraints

2 constraints

Neither of these is a preference. Both are facts about the community that the architecture had to be built around, and the first one is the whole reason the platform exists in the shape it does.

01

Most members have no email address

Every off-the-shelf auth product is built around an email address. This community largely does not have one — the phone number is the identity people actually carry and actually remember.

That single fact ruled out the default path and is why the platform has a custom phone-number credential layer. It is also why the platform got adopted at all: a signup flow that opens with "enter your email" is a signup flow most of this community cannot finish.

02

Families register together, not individually

An adult signs up and adds their spouse, children, and parents. Treating the individual as the unit of registration would have meant a login for every grandparent and a directory full of accounts nobody would ever sign into.

So the household is the unit, not the person. Today 79 households cover 293 people — 86% of all member records — behind 136 login accounts. The gap between 342 records and 136 logins is not padding; it is the household model doing exactly what it was built to do.

Decisions

8 decisions

Each of these started as a constraint or a failure, not a preference. The order is roughly the order they came up.

01

Phone auth on top of Supabase Auth, not instead of it

Phone number plus password, with the password bcrypt-hashed in the browser and stored against a synthetic email address. That synthetic address is the hinge: it lets Supabase Auth, the credentials table, and the directory record all share a single UUID, so there is one identity across three tables and nothing to reconcile later.

Google OAuth runs alongside it with account identity linking, so a member who signed up by phone can attach a Google account later and sign in either way. The synthetic-email trick is what let a phone-first flow sit on an email-first auth provider without forking it — session handling, refresh tokens, and row-level security integration all still come from Supabase.

02

Authorization lives in PostgreSQL, not the client

Client-side role checks are a UX affordance. The real gate has to survive a hand-crafted API request typed into the browser console, because eventually someone will type one.

47 row-level security policies cover the tables, and every privileged operation goes through one of 20 security-definer RPCs. The three-tier model — user, admin, superadmin — is driven by a pure permission matrix that is unit-tested on its own, with nothing mocked and no network in the loop. Registration windows are enforced by database triggers, so a write to a closed event is rejected in Postgres no matter what the client believes about the deadline.

03

Signup is one transaction

The original flow was three sequential writes from the client: auth user, credentials row, directory record. A failure between step one and step two left an orphaned auth user with no profile attached and no way to recover — the member could not sign in, and could not sign up again with the same phone number either.

It is now a single Postgres function. All three rows commit or none of them do. Admin user creation and password changes got the same treatment.

04

The 1,000-row bug that looks like success

PostgREST silently caps SELECT results at 1,000 rows and returns no error when it does. On an admin screen whose entire purpose is answering "who hasn't registered yet," quietly dropping everyone past row 1,000 is the worst failure mode available: the page renders, the counts look plausible, and the people missing from the list are precisely the ones you opened it to find.

Every whole-table read now routes through a pagination helper that keeps requesting pages until it gets back a short one.

05

Two-tier caching with stampede protection

An in-memory session cache sits over a localStorage stale-while-revalidate layer. The in-memory tier deduplicates in-flight requests — concurrent callers await the same promise instead of each firing its own query — so a page that mounts six components all needing the member list issues one request rather than six.

Both tiers clear on sign-out. This runs on shared family computers, and the next person to sign in should not be able to see the previous person's data.

06

Real-time without the socket sprawl

Live updates arrive over Supabase WebSocket channels backed by Postgres logical replication. When another user edits a record, open lists patch that row in place instead of refetching the table.

Subscriptions are gated on auth state, so an anonymous visitor opens none at all. The navbar reuses the stream a page has already opened by way of a DOM event, rather than opening a second connection to a table somebody is already watching.

07

Retire by flag, not by deletion

Concluded events and disabled features stay in the codebase, dormant, with their data intact and still exportable. Their routes unmount, so an old bookmark returns a 404 rather than silently accepting a submission into an event that ended months ago.

Bringing a surface back is a config change, not a revert.

08

The parts users actually feel

89 kB gzipped of initial JavaScript, via route-level code splitting across 12 pages, with the heavy dependencies — Leaflet, bcrypt, the CSV parser — lazy-loaded only when something needs them. Build-ID polling prompts a reload after a deploy so nobody is stranded on a stale bundle halfway through a registration.

Motion runs on a hand-written spring engine: semi-implicit Euler integration on fixed 4 ms substeps, with velocity-preserving retargeting so an animation can be redirected mid-flight instead of snapping when a gesture interrupts it. Async feedback is announced through aria-live regions, and prefers-reduced-motion is honored in both the CSS and the JS animation loop.

Security headers are verified live in production: HSTS preload, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and X-Frame-Options.

Where it stands

Launched in April 2026 and in daily use since. Membership went from zero to 280 in the first eight weeks and reached 342 records by August 2026 — 340 of them active, spread across 45 states and 93 cities.

17 events have taken 463 registrations, including a 14-city national tour, carrying 839 attendee-date selections and 232 accommodation requests. The largest single event drew 71 registrations, 67 of which needed lodging; the busiest day took 56. Three admins have performed 184 audit-logged privileged actions across 13 action types, and 15 gathering locations are mapped across 11 states.

Codebase
78 files · ~26k lines frontend · ~3.1k lines SQL
Database
9 tables · 43 versioned migrations
Tests
68 unit tests (Vitest)
History
140 commits · sole developer

Stack

Frontend
React 19Vite 8React Router 7LeafletOpenStreetMap
Data & backend
SupabasePostgreSQLPL/pgSQLRow-Level SecurityLogical replication
Auth
Supabase AuthbcryptGoogle OAuthIdentity linking
Testing & ops
VitestVercelGrafana Cloud
Reach out

Got a question? Let's talk.