Skip to content

Architecture

Luara edited this page Jul 7, 2026 · 7 revisions

Architecture

Overview

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.


System Diagram

              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

Tech Stack

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

Folder Structure

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/         # 83 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

Key Data Flows

Anonymous user viewing a colony

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)

Caretaker accessing an exact location

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

File upload

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)

Key Architecture Decisions

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.

Clone this wiki locally