The problem
A book fair at the public library, a neighbourhood festa junina, a street race, aboi de mamão performance — these events are free, run by city departments, libraries and collectives, and have no ticket to sell. National ticketing platforms ignore them, so they stay invisible outside word of mouth. There was no single place to answer "what is happening near me this weekend" for a specific region.
What I built
A full platform with three roles:
- Visitors browse and filter events by city, category, date, price and format; a "near me" mode sorts by real distance to the venue and shows everything on a map.
- Organizers create an account and submit events for free; they land in a moderation queue and the organizer tracks status, rejection reason and per-event stats.
- The team moderates the queue, manages cities from an embedded IBGE municipality list, reads contact messages, and watches a metrics panel (events by city, category, state and month).
It ships as an installable PWA and exposes a small public read API so other regional sites can embed the agenda.


Decisions & trade-offs
- One codebase, two runtime modes
- Without Supabase env vars the app runs in demo mode — data from local JSON, a fake session, writes to
localStorage— so the whole thing (organizer and team flows included) runs with no backend. Made local development and review trivial. - Security in the database, not just the app
- Row Level Security on every table: the public anon key is safe precisely because the policies constrain it. Anyone can insert an event, but only as
pending; only the team can change status; the moderator allowlist is a table where each row is visible only to its own owner. - "Near me" measures the venue, not the city
- The first version measured GPS → city centroid, which produced nonsense like"5 km from Jaraguá do Sul, from you". I added per-event coordinates (geocoded on submit) and a Haversine distance to the actual venue, with the city as fallback. A real correctness bug, caught by using it.
- Prerendered HTML for a SPA
- A post-build step renders static HTML per event and per city, plus
sitemap.xml, so search engines and link unfurlers get real content and a proper preview card — not an empty React shell. - Email that degrades quietly
- Transactional email runs from a serverless function as best-effort: no SMTP configured, or an organizer contact that is a phone number → nothing is sent and moderation works exactly the same.
- One source of truth for seed data
- A single script is the origin of the sample data; JSON, SQL seeds and placeholder art are all generated from it, so nothing drifts out of sync.

Where it landed
- 27 seed events across 16 cities and 9 states; live on Vercel as a PWA.
- 68 unit tests over the pure logic (distance math, filters, metrics, RSVP), run in CI on every push.
- Accessibility built in: semantic structure, keyboard-operable menus, labelled inputs, charts backed by a data table,
prefers-reduced-motionhonoured.
Stack
- React 18
- Vite
- React Router
- Tailwind CSS
- Supabase (PostgreSQL + Auth + Storage)
- PostgREST
- Vercel serverless (Node)
- Leaflet + OpenStreetMap
- vite-plugin-pwa
- Vitest
- GitHub Actions
What I'd do next
Scheduled email ("your event is in 2 days", "new event in your city") withpg_cron and a subscriber list; a payment gateway for paid highlights (currently a manual Pix flow); and onboarding tools for a city tourism department to run its own official agenda.
