Engineering
API design
Mostly Supabase auto-generated REST + a handful of Edge Functions for orchestrated logic (GPS verification, feed ranking, AI). Authentication via JWT bearer in Authorization header.
Endpoint map
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /auth/v1/signup | — | Email signup (Supabase) |
| POST | /auth/v1/token?grant_type=password | — | Login |
| POST | /auth/v1/recover | — | Forgot password |
| POST | /auth/v1/token (apple/google) | — | OAuth sign-in |
| GET | /rest/v1/energy_markers | JWT | List/filter markers |
| POST | /functions/v1/create-marker | JWT | GPS-verified marker creation |
| GET | /rest/v1/energy_markers?id=eq.{id}&select=*,marker_photos(*),marker_ratings(*) | JWT | Marker detail |
| POST | /rest/v1/marker_ratings | JWT | Submit rating |
| POST | /functions/v1/feed | JWT | Personalized discovery feed |
| GET | /functions/v1/search?q=&lat=&lng=&category= | JWT | Search + autocomplete |
| POST | /rest/v1/visits | JWT | Log a visit (GPS-verified) |
| POST | /rest/v1/journal_entries | JWT | Create journal entry |
| POST | /rest/v1/followers | JWT | Follow user |
| GET | /rest/v1/notifications?user_id=eq.&order=created_at.desc | JWT | List notifications |
| POST | /functions/v1/ai-recommendations | JWT (premium) | Phase 2 AI feed |
Edge function: createMarker
POST /functions/v1/create-marker
Authorization: Bearer <jwt>
Content-Type: application/json
Request:
{
"title": "Sunrise at Ubud Monkey Forest",
"description": "Pure stillness at 6am.",
"category": "spiritual",
"intensity": 9,
"mood": "awe",
"lat": -8.5189,
"lng": 115.2588,
"gps_accuracy_m": 12,
"photos": ["temp/abc123.jpg", "temp/def456.jpg"]
}
200 OK:
{
"marker": {
"id": "uuid",
"energy_score": 0,
"created_at": "2026-05-31T10:24:00Z"
}
}
422 Unprocessable (GPS):
{ "error": "gps_too_far", "distance_m": 240 }
429 Too Many:
{ "error": "rate_limit", "retry_after_s": 3600 }Edge function: feed
POST /functions/v1/feed
{
"lat": 40.7128, "lng": -74.0060,
"rails": ["trending","nearby","new","powerful","hidden_gems"],
"cursor": null,
"limit": 10
}
→ Returns one page per rail in a single round-trip.
Ranking signals (MVP):
trending = score * recency_decay * recent_visit_count
nearby = ST_Distance(point, user) asc
new = created_at desc, score > 6
powerful = energy_score desc, ratings_count >= 10
hidden_gems = energy_score desc, visits_count between 3 and 25Realtime channels
// Live marker counts on the map for a viewport
supabase.channel('viewport:{geohash5}')
.on('postgres_changes', {
event: 'INSERT', schema: 'public', table: 'energy_markers',
filter: 'geohash5=eq.{viewport}'
}, handleNewMarker)
.subscribe();
// Notifications
supabase.channel('user:{userId}:notifications')
.on('postgres_changes', { event: 'INSERT', schema: 'public',
table: 'notifications', filter: 'user_id=eq.{userId}' },
pushToTray)
.subscribe();Error contract
| HTTP | Code | Meaning |
|---|---|---|
| 400 | validation_error | Schema or value out of range |
| 401 | unauthenticated | Missing/expired JWT |
| 403 | forbidden | RLS or premium gate |
| 404 | not_found | Resource missing |
| 409 | conflict | Duplicate rating, follow, etc. |
| 422 | gps_too_far | Outside 100m radius |
| 429 | rate_limit | Quota exceeded |
| 500 | internal | Unhandled — Sentry captures |
Versioning & deprecation
Mobile app pins to a major Edge Function version via the
X-Client-Version header. Breaking changes ship behind a new path (/functions/v1/feed → /v2/feed) with a 6-month overlap and remote-config kill switch.