Account-free RSVP for kids' birthday parties — host creates a party with standard or custom questions; each family RSVPs once listing one or more children; the host gets a live headcount and a venue-ready summary.
- Create a party in seconds — no signup, no email.
- Share a guest link via WhatsApp or any channel. Each family RSVPs once and can list multiple children.
- Standard one-click questions: adult count, allergies, date of birth, preferred snack.
- Custom questions per family or per child — choice (dropdown), free text, number, or date.
- Watch RSVPs arrive live; the headcount updates in real time.
- Export a venue-ready summary (WhatsApp-formatted) with every family's answers.
- Hybrid add-to-calendar:
.icsfile on iOS and desktop, Google Calendar link on Android. - Six languages: Spanish, English, Galician, Basque, Catalan, Valencian.
- Install it as a PWA on any device.
- No direct table access — anonymous clients cannot read, list, insert, or update the
partiestable directly. Row-Level Security denies all direct access; every operation goes throughSECURITY DEFINERRPCs. There is no bulk read: a party can only be fetched by its exact id (get_party), so the anon key cannot enumerate or dump other families' data. - Server-side edit PIN — when a party has an edit PIN, config edits and PIN changes are verified server-side (the PIN hash is computed in Postgres and never shipped to clients). A party without a PIN stays editable by anyone holding its link — that is by design, to keep the no-signup flow; treat the host link as the capability and keep it private.
- Bounded writes — RSVP submissions are capped (per-RSVP size, RSVP count, and total blob size) to prevent storage/egress abuse. RSVP appends are atomic, so concurrent families never clobber each other.
- No cookies, no tracking, no ads — so no consent banner. An in-app privacy notice (Footer → Privacy) states what's stored and links the erasure path.
- Erasure — the host can hard-delete a party (and all family data) any time via "Delete party" (
delete_partyRPC, PIN-gated). This is the data-subject erasure mechanism. - Retention — a daily
pg_cronjob (monete-retention) deletes parties 90 days after the event date. - Personal data lives only in
parties.data(child names, optional DOB + allergies, parent label); the table isCOMMENT-tagged as holding special-category (health) data of minors.
- A PIN-less party is fully open to link-holders. Anyone with the link can edit it, and can set the first PIN themselves — so the no-signup model can't stop a malicious link-holder from locking a host out of a party the host left PIN-less. Set a PIN if that matters.
- Online PIN guessing is not rate-limited. The 4–6 digit PIN is verified server-side and the hash never ships, so offline cracking is gone — but a link-holder can still try PINs against the API without a lockout. The PIN raises the bar for a link-holder; it is not a strong secret. Keep the host link private regardless.
Open-source, MIT licensed. PRs welcome.
Stack — React 19, Vite, TypeScript (strict), Tailwind CSS 4, Supabase (Postgres + Realtime), vite-plugin-pwa, i18next, Vitest.
Architecture — Hexagonal (domain → application → infrastructure → presentation). DI via a hand-rolled container. Tests use in-memory fakes — no DB mocks.
git clone git@github.com:Endika/Monete.git
cd Monete
cp .env.example .env.local # fill VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY
npm install
npm run devApply the migration once to your Supabase project:
supabase db pushOr paste the files in supabase/migrations/ (in order) directly into the Supabase SQL editor. 0004_harden_access.sql closes direct table access and routes everything through RPCs — apply it on top of the earlier migrations.
| Command | Description |
|---|---|
npm run dev |
Start the dev server |
npm run build |
Production build (tsc -b && vite build) |
npm run test:run |
Run tests once |
npm run lint |
ESLint (zero warnings) |
npm run type:check |
TypeScript type check |
CI runs lint, typecheck, tests, and the production build on every PR.
Copy .env.example to .env.local and fill in your Supabase project values:
| Variable | Description |
|---|---|
VITE_SUPABASE_URL |
Your Supabase project URL |
VITE_SUPABASE_ANON_KEY |
Your Supabase anon (public) key |
VITE_GOOGLE_MAPS_KEY |
Google Maps/Places key (optional, address + map) |
Restrict the Google Maps key.
VITE_GOOGLE_MAPS_KEYships in the client bundle by design, so it cannot be kept secret. Lock it down in the Google Cloud Console or anyone can run up your bill: set an HTTP-referrer restriction to your production domain, an API allowlist (Maps Embed + Places API only), and a billing budget + alert. This control lives in GCP — the code cannot enforce it.