-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Felines uses Supabase as its backend. Most data access happens through standard Supabase queries with Row Level Security enforcing permissions automatically. Sensitive operations use server-side RPCs that validate permissions before executing.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)const { data } = await supabase
.from('colonies')
.select(`
id, name, narrative,
latitude_blurred, longitude_blurred,
castration_status, health_status,
cover_photo_url, verified_status, created_at
`)
.is('removed_at', null)Returns blurred coordinates only. Exact coordinates are never
returned by public queries. removed_at filters out any colony
removed by the 3-false-pin-flag ban system.
const { data } = await supabase
.from('colonies')
.select(`
*,
cats(*),
caretakers(user_id, profiles(display_name, avatar_url)),
timeline_events(*)
`)
.eq('id', colonyId)
.single()Static content in lib/articles.ts — no database query needed.
import { ARTICLES } from '@/lib/articles'
const article = ARTICLES.find(a => a.slug === slug)const { data } = await supabase
.from('community_contacts')
.select('id, city, name, phone, email, social, category, notes, created_at')
.order('city')const { data } = await supabase
.from('colonies')
.insert({
name: 'Colony name',
narrative: 'Colony story',
latitude: -5.7945, // exact
longitude: -35.2110,
latitude_blurred: -5.800, // ~500m offset (anon)
longitude_blurred: -35.216,
latitude_blurred_near: -5.7950, // ~100m offset (authenticated)
longitude_blurred_near: -35.2115,
castration_status: 'unknown',
created_by: session.user.id,
cover_photo_url: 'https://...'
})
.select('id')
.single()Blocked if is_user_banned(auth.uid()) returns true (see
Security).
const { data } = await supabase
.from('feedings')
.insert({ colony_id: colonyId, type: 'food', user_id: userId }) // or 'water'const { data } = await supabase
.from('reports')
.insert({
colony_id: colonyId, // optional
type: 'sighting', // one of 9 types
description: '...',
latitude: -5.7945,
longitude: -35.2110,
})Report types: sighting, no_food_water, injured_cat,
missing_cat, new_kitten, disease_outbreak,
suspected_poisoning, suspected_abuse, threat_to_colony
Coordinates are rejected by a database CHECK constraint if out
of geographic range, regardless of caller.
All RPCs validate permissions internally before executing — never trust a client-supplied role or flag.
const { data } = await supabase.rpc('get_colony_exact_location', {
p_colony_id: colonyId
})
// Returns: { latitude, longitude } or permission deniedRequires: Authenticated + linked caretaker or creator. Re-validates the link server-side on every call.
const { data } = await supabase.rpc('confirm_report', {
p_report_id: reportId
})Requires: Authenticated. Blocks: self-confirmation, duplicate confirmation from the same user.
const { data } = await supabase.rpc('mark_cat_seen_today', {
p_cat_id: catId
})Requires: Authenticated.
const { data } = await supabase.rpc('thank_action', {
p_timeline_event_id: eventId
})Requires: Authenticated. Blocks: self-thanking.
await supabase.rpc('record_daily_visit')
await supabase.rpc('record_care_streak', { p_colony_id: colonyId })Requires: Authenticated. Idempotent — safe to call more than once per day; resets to 1 (not 0) after a missed day.
await supabase.rpc('notify_caretakers', {
p_colony_id: colonyId,
p_type: 'report_submitted',
p_report_type: null // required only for 'area_alert'
})Requires: anon or authenticated (intentionally — anonymous report submission needs to trigger it). Message is built server-side from a fixed template; the caller can never inject free text. Rate-limited via a database circuit breaker (one batch per colony per 5 minutes).
const { data } = await supabase.rpc('get_own_streak')
// Returns: { current_streak, longest_streak } for the calling user onlyRequires: Authenticated. The only way to read streak data — the underlying columns have no public grant.
Photos are stored in Supabase Storage in the colony-photos bucket.
import { validatePhotoFile, buildSafeStoragePath, assertSafeStoragePath } from '@/lib/storage'
const error = validatePhotoFile(file) // MIME + size check
if (error) throw new Error(error)
const path = buildSafeStoragePath(`colonies/${colonyId}`, file)
assertSafeStoragePath(path) // belt-and-suspenders re-check
const { data } = await supabase.storage
.from('colony-photos')
.upload(path, file)Public read URL:
const { data } = supabase.storage.from('colony-photos').getPublicUrl(path)const url = new URL('https://api.openweathermap.org/data/2.5/weather')
url.searchParams.set('lat', String(lat))
url.searchParams.set('lon', String(lon))
url.searchParams.set('appid', process.env.NEXT_PUBLIC_WEATHER_API_KEY!)
url.searchParams.set('units', 'metric')
url.searchParams.set('lang', language === 'pt' ? 'pt_br' : 'en')import { validateCoordinates } from '@/lib/validateCoordinates'
import { reverseGeocode } from '@/lib/geocode'
const safe = validateCoordinates(lat, lon) // throws if invalid
const cityName = await reverseGeocode(safe.lat, safe.lon, 'en')Coordinates are validated (range, NaN, Infinity) before any
request is built, and the request uses URLSearchParams with
redirect: "error" — see Security.