Clinic management web app for People Helping Pets, a Chicago nonprofit that runs free bilingual (English/Spanish) vaccine clinics for cats and dogs in underserved neighborhoods.
Loopy covers the whole clinic day and what comes after: client and pet registration, vaccine and microchip logging, rabies certificates, event close-out, follow-up and reminder emails, and reporting.
| Layer | Tool |
|---|---|
| Frontend | React 19 + TypeScript, built with Vite (clinic-web/) |
| Routing | React Router |
| Database, auth, storage | Supabase (Postgres + RLS, Auth, Storage) |
| Hosting + backend | Vercel (static site + serverless functions in api/) |
Resend, with signed webhooks verified by svix |
|
| Translation | DeepL (free tier), used for Spanish breed/color fields on rabies certificates |
| PDFs | pdf-lib (certificates and waivers, generated in the browser) |
| Signatures | signature_pad (waiver capture) |
clinic-web/ React app (src/, public/, Vite config)
api/ Vercel serverless functions
_auth.js shared login/role check for protected endpoints
admin-users.js invite, list, and set roles for users (admin only)
send-postclinic.js post-clinic follow-up emails
send-reminders.js daily reminder run (cron)
send-reminder-manual.js manual reminder sends from the Reminders tool
translate.js DeepL proxy for certificates
webhooks/resend.js Resend delivery/bounce/complaint events
supabase/
migrations/ schema changes, in order
schema.sql schema snapshot (see note under Database)
docs/ architecture notes
.github/workflows/ build check, Supabase keep-alive
vercel.json build settings, redirects, SPA rewrite, cron
package.json root dependencies for the serverless functions (svix)
Sign-in uses Supabase Auth. Each user has a role in their JWT (app_metadata.role): volunteer, staff, or admin.
| Route | Who | Purpose |
|---|---|---|
/ |
everyone | Landing page |
/registration |
public | Bilingual client and pet registration with waiver signature |
/login, /set-password |
everyone | Sign-in and invite password setup |
/clinic |
signed in | Single clinic-day page: walk-ins, check-in, vaccine and microchip logging, print-record links, attendance history |
/client/:id |
signed in | Client profile: inline editing, vaccination history, waiver PDF, certificates |
/client/:id/print/:petId |
signed in | Printable pet record |
/records |
signed in | Searchable client and pet database |
/reports |
staff, admin | Analytics (vaccines, species, ZIP demographics, return rates, waste) |
/staff-home |
staff, admin | Events, vaccine lot inventory, clinic stats |
/tools/reminders |
staff, admin | Email templates, triggers, manual sends, failures and bounces |
/tools/batch-certs |
staff, admin | Generate rabies certificates in bulk |
/tools/merge-clients |
staff, admin | Merge duplicate client records |
/tools/user-management |
admin | Invite users and set roles |
/intake and /discharge redirect to /clinic. The old /volunteer-home page was removed; volunteers now land on /clinic after signing in, and old links redirect there. Old .html URLs (for example /records.html) redirect to their React routes.
- Public registration through the
register_visitdatabase function, so the public form needs no direct table access. - Duplicate-aware client handling, with accent-insensitive search (
unaccent) and a merge tool. - Rabies certificates (NASPHV Form 51) generated in the browser from a template in Supabase Storage.
- Event close-out that prompts for vaccine waste before an event is finalized.
- Vaccine lot inventory with expiration tracking.
- Email system: post-clinic follow-ups and reminders sent in batches through Resend, with per-send failure logging, bounce/complaint tracking from webhooks, and a dynamic trigger system (microchip status, intact pets, ZIP code, upcoming due dates).
- Reminder cron runs daily at 10:00 UTC (see
vercel.json).
Schema changes live in supabase/migrations/. Apply new ones to the live project deliberately; there is a separate dev project for testing.
supabase/schema.sqlis a snapshot taken before the October 2026 access lockdown, so it still shows the old anonymous policies. The migration20261024000000_close_anon_access.sqlrecords that lockdown. Use the live database, or the migrations, as the source of truth.
RLS is enabled on every table.
- Anonymous visitors can only call
register_visit(the public registration form) and readevents. They have no direct access toclients,pets,vaccinations, or any email table. - Signed-in users read and write clinic data according to their role. Deleting clients and pets is admin-only.
- Email and trigger tables (
email_events,email_send_failures,postclinic_email_log,reminder_email_log,triggers) are readable only by signed-in users. Writes come from serverless functions using the service-role key, which bypasses RLS. - The
pet_next_dueview runs with the caller's permissions (security_invoker).
A public bucket named templates holds the NASPHV Form 51 certificate template and the assets used in emails (logo and take-home sheets). The browser fetches the template when a certificate is generated. A separate private bucket named signatures holds the veterinarian signature image; signed-in users can read it (see supabase/migrations/20261026000000_private_signatures_bucket.sql), and nobody else can.
Requires Node 20+.
cd clinic-web
npm install
npm run dev
Copy clinic-web/.env.example to clinic-web/.env.local (gitignored) and fill it in:
VITE_SUPABASE_URL=https://<project-ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<anon key>
VITE_VET_LICENSE_VALBUENA=<license number shown on certificates>
Use the dev Supabase project's values for local work. Other scripts: npm run build (type-check and build), npm run lint, npm run preview.
The api/ functions only run on Vercel (or with vercel dev).
Pushing to main deploys through Vercel. vercel.json installs and builds clinic-web/, serves clinic-web/dist, rewrites every non-/api path to the SPA, and registers the daily reminder cron.
GitHub Actions run a build check on every pull request and push to main, plus a twice-weekly keep-alive ping so the free-tier Supabase project doesn't pause.
See .env.example for a copy-paste template.
| Variable | Used by |
|---|---|
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY |
React app (public by design; protected by RLS) |
VITE_VET_LICENSE_VALBUENA |
Certificate generation |
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY |
Serverless functions (never expose to the browser) |
RESEND_API_KEY, RESEND_WEBHOOK_SECRET |
Email sending and webhook verification |
CRON_SECRET |
Authorizes the scheduled reminder run |
DEEPL_API_KEY |
Certificate translation |
SITE_URL |
Where invite links land |
Loopy began as a vanilla HTML/CSS/JavaScript site (one page per tool, config.js generated at build time). It was replaced by the React app in October 2026. The last version of that site is preserved in the Git tag live-before-migration.
To use it as a fallback you would need more than the code. The old pages talked to the database as the anonymous role, and that access was closed on Oct 24, 2026. A rollback means re-creating the old anonymous policies on clients, pets, and vaccinations, and restoring the old build variables (SUPABASE_ANON_KEY, VET_LICENSE_VALBUENA). Only do that deliberately, since it reopens public read access to client data.
- Registration allows up to 3 pets per visit.
- Validation is mostly client-side; the database and
register_visitenforce the essentials. - Barcode scanning for vaccine vials, annual renewal reminders, and spay clinic support are planned but not built yet.
A short diagram of how the browser, Supabase, the serverless functions, Resend and DeepL fit together is in docs/ARCHITECTURE.md.
Loopy is licensed under the GNU Affero General Public License v3.0. In short: you can use and modify it, but if you run a modified version as a network service, you must offer its source code to the people using that service.
Please report vulnerabilities privately; see SECURITY.md. Don't open a public issue for security problems.