Energy Atlas
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

MethodPathAuthPurpose
POST/auth/v1/signupEmail signup (Supabase)
POST/auth/v1/token?grant_type=passwordLogin
POST/auth/v1/recoverForgot password
POST/auth/v1/token (apple/google)OAuth sign-in
GET/rest/v1/energy_markersJWTList/filter markers
POST/functions/v1/create-markerJWTGPS-verified marker creation
GET/rest/v1/energy_markers?id=eq.{id}&select=*,marker_photos(*),marker_ratings(*)JWTMarker detail
POST/rest/v1/marker_ratingsJWTSubmit rating
POST/functions/v1/feedJWTPersonalized discovery feed
GET/functions/v1/search?q=&lat=&lng=&category=JWTSearch + autocomplete
POST/rest/v1/visitsJWTLog a visit (GPS-verified)
POST/rest/v1/journal_entriesJWTCreate journal entry
POST/rest/v1/followersJWTFollow user
GET/rest/v1/notifications?user_id=eq.&order=created_at.descJWTList notifications
POST/functions/v1/ai-recommendationsJWT (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 25

Realtime 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

HTTPCodeMeaning
400validation_errorSchema or value out of range
401unauthenticatedMissing/expired JWT
403forbiddenRLS or premium gate
404not_foundResource missing
409conflictDuplicate rating, follow, etc.
422gps_too_farOutside 100m radius
429rate_limitQuota exceeded
500internalUnhandled — 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.