Trailside membership lookup — technical & privacy brief
Central Washington chapter · prepared for the statewide membership committee / IT review · June 2026
A volunteer-run trailhead booth tool that lets a rider check their Evergreen membership status and renew or join on the spot. This brief covers how it’s hosted, the privacy & security model, and what a production / statewide implementation would look like — including hosting, data refresh, and payment. The guiding principle: the member roster never leaves the database, and the public web page contains no member data whatsoever.
Architecture (current)
Serverless / edge. No servers to patch or maintain. All traffic over HTTPS/TLS.
Volunteer device
Any browser/iPad. Static page only — holds zero member data.
Cloudflare Pages
Static kiosk (HTML/JS) on global CDN. No database access.
Supabase Edge Function
Deno. Runs as service role, does the fuzzy match, returns one record.
Postgres · members
Row-Level Security ON, no read policy — unreadable to the public key.
A lookup never queries the table directly. The browser calls an edge function with a signed session token; the function (and only the function) reads the table using a privileged key and returns only the single matched person — or a short chooser list with no contact info. Bulk export through the app is structurally impossible.
Security & privacy controls
- Database locked by RLS — Row-Level Security is enabled on the members table with no SELECT policy, so the public/anon API key returns zero rows (verified). Reads only happen inside the edge function via the service-role key.
- Single-record responses — the lookup returns one matched member (or up to ~5 name candidates with no email/phone). There is no “list all” path.
- Booth authentication — volunteers enter a shared booth code; a separate edge function validates it server-side and issues a short-lived (12-hour) HMAC-signed JWT. The code/secret is never in the page source; the lookup function rejects any request without a valid token.
- Rate limiting — per-IP limits on lookups (60/min) and auth attempts (20/min) prevent enumeration and brute-forcing.
- Data minimization — only booth-needed fields are stored/returned (name, type, city, member-since, expires, email, auto-renew). Phone/zip/IDs are not loaded. Local device logs store result counts, not member names.
- No payment data, no PCI scope — the kiosk never touches card data. Payment happens entirely on Evergreen’s existing processor (CiviCRM); we only deep-link to it.
- Transport security — HTTPS/TLS end to end (Cloudflare + Supabase). The roster is a derived cache of the membership system of record, not the source of truth.
Where it lives today → recommended for production
| Layer | Today (prototype) | Recommended for Central / statewide |
| Hosting | Cloudflare Pages, free tier (built by the volunteer) | Move to an Evergreen-owned Cloudflare account; IT holds credentials & billing |
| Database | Supabase (Postgres), us-west-1, free tier, in the builder’s org | Re-create in an Evergreen-owned Supabase org (1-time migration) |
| Domain | cw-evergreen-booth.pages.dev | members.cwevergreenmtb.org (or statewide) via Cloudflare DNS, auto-TLS |
| Roster scope | Central chapter only | Statewide roster — same schema; chapter is just a column |
Implementation recommendations
- Ownership transfer — stand up the Cloudflare + Supabase projects under Evergreen-controlled accounts so IT owns access, billing, and rotation.
- Automated data refresh — replace the manual CSV import with a scheduled weekly sync (CSV drop, or ideally a read-only CiviCRM API pull) so the roster stays current.
- Auth tier — shared booth code today; can upgrade to per-volunteer logins (Supabase Auth) for accountability/revocation if desired.
- Backups — low risk (the DB is a cache); Supabase managed backups on a paid tier, or simply re-import from CiviCRM.
- Statewide rollout — one shared instance; each chapter’s booth filters by location/event. No per-chapter rebuild.
Payment recommendations
- Enable digital wallets (Apple Pay / Google Pay) on the processor — if Stripe-backed, this is a configuration toggle (Payment Request Button), not custom development. Biggest friction win.
- Express CiviCRM page — a standard CiviCRM contribution page with chapter preset, minimal fields, and no login; CiviCRM dedupes by email for renewals.
- QR deep-link + UTM — the kiosk sends riders to that page on their own phone, tagged for attribution (already built). Confirm GA records the UTMs.
- Card-present option — Stripe Tap to Pay / Square if a wallet isn’t available.
- Scope note — all payment stays on Evergreen’s PCI-compliant processor; the booth tool is out of PCI scope entirely.
Stack & cost
Cloudflare Pages (static + CDN)
Supabase Postgres + Row-Level Security
Deno edge functions
HMAC-signed JWT auth (Web Crypto)
No backend servers
~$0/mo on free tiers
What we’d need from Central / statewide IT
- A decision on account ownership (Evergreen-held Cloudflare + Supabase) and a DNS record for a custom domain.
- A data feed — a weekly membership export, or read-only CiviCRM API access for an automated sync.
- Payment config — digital wallets + an express/no-login contribution page in CiviCRM, and confirmation that GA captures the UTM tags.
- Sign-off on the data-handling model above (roster as a locked, single-record-lookup cache).