-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Felines is a full-stack web application built on Next.js 16
(App Router, Turbopack) with Supabase as the complete backend.
The deploy runs on Netlify with continuous deployment via GitHub.
There is no custom backend server — every write and read goes
through Supabase's auto-generated REST API and RPC endpoints,
gated entirely by RLS policies and SECURITY DEFINER functions.
The Next.js layer never holds a service-role key; it uses the
same public anon key a browser would, which means the security
boundary is the database itself, testable directly with curl.
Browser / Mobile Web
↓
Next.js 16 (Netlify)
App Router · TypeScript · Tailwind CSS
↓
Supabase (PostgreSQL)
Auth · Storage · RLS · RPCs
↓
External APIs
OpenWeatherMap · Nominatim (OSM)
Map Layer (client-side)
Leaflet.js · OpenStreetMap tiles
| Layer | Technology | Why |
|---|---|---|
| Frontend | Next.js 16 (App Router, Turbopack) + TypeScript + React 19 | Server components keep public-page data-fetching close to the database; strict typing catches whole classes of bugs before runtime |
| Styling | Tailwind CSS v4 (CSS-based @theme) |
Fast iteration without leaving the markup |
| Map | Leaflet.js + react-leaflet + react-leaflet-cluster
|
Open source, no API key or usage quota, full control over pin styling and the blur-circle overlay |
| Database | Supabase (PostgreSQL) | Row Level Security lets sensitive rules live in the database itself |
| Auth | Supabase Auth | Email/password wired directly into Postgres' auth.users
|
| Storage | Supabase Storage | Server-side size/MIME enforcement, RLS on write access |
| i18n | Custom PT/EN context (lib/i18n) |
The translation surface is small and finite — a lightweight context avoids a general-purpose framework's overhead |
| Deploy | Netlify | Native Next.js support, automatic CD from GitHub |
| Weather | OpenWeatherMap API | Free tier, real coordinates instead of a fixed city |
| Geocoding | Nominatim (OpenStreetMap) | Free, no API key |
felines/
├── app/ # Next.js App Router — 21 page routes
│ ├── page.tsx # Home — hero, impact stats, entry points, the guide
│ ├── map/ # Interactive colony map
│ ├── colony/[id]/ # Colony detail — 5 tabs (cats, timeline, needs, reports, letter)
│ ├── colony/new/ # Register a colony
│ ├── learn/[slug]/ # Individual educational article (19 total)
│ ├── glossary/, plants/, curso/ # Glossary, toxic plants, caretaker course
│ ├── reports/ # Reports / resources / contacts / stories hub
│ ├── impact/ # Public platform-wide impact statistics
│ ├── notifications/ # Notification inbox
│ ├── profile/, u/[id]/ # Signed-in profile, public caretaker profile
│ └── login/, signup/ # Auth
├── components/ # ~90 single-concern React components, grouped by domain
│ ├── assistant/, auth/, colony/, impact/, learn/
│ ├── map/, profile/, reports/, resources/, shared/, stories/
├── hooks/
│ └── useFelinesAssistant.ts # Cat assistant trigger logic — 9 distinct triggers
├── lib/
│ ├── content/ # Static content sources (articles, glossary, quizzes, type enums)
│ ├── security/ # storage.ts, safeReturnTo.ts, rateLimit.ts, validateCoordinates.ts
│ ├── data/ # Supabase read/write helpers (notifications, profile, reports)
│ ├── external/ # Third-party API wrappers (geocode.ts, weather.ts, siteUrl.ts)
│ ├── i18n/pt.ts, en.ts # Full Portuguese and English translations
│ └── supabaseClient.ts # Shared Supabase client (anon key, used client- and server-side)
├── supabase/migrations/ # 86 numbered SQL migrations (schema, RLS, RPCs) — 26 tables
├── docs/AUDIT_REPORT.md # Full itemized security/bug/refactor audit
├── docs/UI_AUDIT_REPORT.md # UI consistency, motion, and mobile audit
├── SECURITY.md # Vulnerability disclosure policy
└── CONTRIBUTING.md # Local setup and contribution guidelines
User opens /map
→ Query returns BLURRED coordinates only
(latitude_blurred, longitude_blurred)
→ RLS prevents exact coordinates from returning
even if requested directly via the REST API
→ User clicks a pin → /colony/[id]
→ Any direct query for the exact coordinate columns:
permission denied (42501)
User's browser calls RPC get_colony_exact_location(colony_id)
→ RPC validates the caretaker/creator link server-side,
against the database, not a client-supplied flag
→ Returns exact coordinates only if validation passes
→ Re-validates the link on every single call —
a stale "am I a caretaker" boolean is never trusted
User selects a photo
→ MIME type validated against an explicit allowlist
(jpeg/png/webp/gif — not a broad "image/*" match,
which would also accept image/svg+xml)
→ Size validated (max 5MB)
→ buildSafeStoragePath() generates the storage path:
a sanitized prefix + a UUID-based filename
→ assertSafeStoragePath() re-checks the path immediately
before the actual .upload() call, as a second guard
→ Upload to Supabase Storage
(RLS: authenticated write, public read)
Why Supabase over Firebase?
RLS means sensitive access rules live as reviewable, testable SQL
instead of scattered client-side checks. Postgres also gives real
relational constraints (foreign keys, CHECK length/range limits,
partial unique indexes) that a document store doesn't offer natively.
Why Leaflet.js over Mapbox or Google Maps? No API key, no usage-based billing risk during a live demo, and full control over the pin styling and blur-circle overlay the location-blur feature depends on.
Why custom i18n over next-intl?
No URL-based locale routing is needed here, so a lightweight
context avoids that complexity entirely. Preference is stored
in localStorage, and the page's lang attribute updates
dynamically to match.
Why progressive blur instead of hiding coordinates entirely? Users need geographic context for the product to make sense. Hiding coordinates completely would break the map experience. Progressive blur balances utility and animal safety at each trust level — see Security for the full mechanism.