PlantAI · how it works

Photo in, plant named, nursery found

Overview

One Node server holds every provider key. The app's only other direct line is Supabase - the session belongs to the user, not the server.

Providers
0
Routes
0
Scraper reads
0
Search time
0
Answer rate
0

End to end: of the shops a user could plausibly be shown, the share that came back with a real product and price. Lower than the read rate because most shops just don't stock the plant.

Measured live · 26 shops read · Tel Aviv 10km · 2026-09-13

Path of one request

  1. App Expo / React Native. No API keys.
  2. Node server One process. Holds every key, spends every credit.
  3. Providers PlantNet, OpenAI, Google Places, Tavily, Firecrawl.
  4. Supabase Account, library, photos - and the scrape cache shared by every user.

The stack, in one picture

Four lines and nothing else. The app talks to one server and to Supabase; only the server talks to a paid provider, because only the server has the keys.

App EXPO 56 · REACT NATIVE 0.85 · TS expo-camera · image-picker expo-image-manipulator expo-location · rn-maps expo-sqlite · file-system expo-notifications react-navigation · reanimated @supabase/supabase-js No provider key ships in the bundle. Logged out, expo-sqlite is the library. Node API server NODE:HTTP · TS · RENDER · :4000 POST /api/diagnose POST /api/nurseries GET  /api/nurseries/job/:id POST /api/care-plan POST /api/translate-diagnosis GET  /health Rate gate · in-memory job store Scraper: parse5 + platform fingerprints Every provider key lives here, and only here. External APIs FIVE PAID PROVIDERS PlantNet my-api.plantnet.org/v2/identify/all OpenAI · gpt-5.6-luna api.openai.com/v1/chat/completions Google Places places.googleapis.com/v1/places:searchText Tavily api.tavily.com/extract Firecrawl api.firecrawl.dev/v1/scrape Supabase POSTGRES · AUTH · STORAGE auth · sessions plants · growth journal photos bucket · signed URLs nursery_searches · shop cache 1 HTTPS 2 API KEYS 3 SCRAPE CACHE 4 DIRECT · NO SERVER

Which API is called, and when

1Diagnose a photo Two providers, one round trip. Nothing is stored on the server.
  1. App resizes the photo, then POST /api/diagnose. Over the size limit is refused before a credit is spent.
  2. PlantNet /v2/identify/all → species, genus, confidence.
  3. OpenAI, only when the species is a coin-flip inside a confident genus → re-reads the photo to settle it.
  4. OpenAI again, photo plus species → condition, issues, treatments, care.
  5. One JSON answer back to the app.
2Nursery search The expensive flow, and the only asynchronous one.
  1. POST /api/nurseries with plant, lat, lng, radius → a job id at once.
  2. Supabase cache checked first. A recent identical search returns results inline and stops here.
  3. Google Places places:searchText → nurseries in the radius, boxed then cut to the true circle.
  4. Tavily /extract reads each shop. Firecrawl /v1/scrape only when the plain read comes back with no product grid.
  5. OpenAI → product and price pulled from the page text.
  6. GET /api/nurseries/job/:id polled until done; the result is written back to the cache for the next user.
3Care plan & Hebrew One model call each, then never again.
  1. POST /api/care-plan → OpenAI once per genus, answering for every growing medium in a single call.
  2. POST /api/translate-diagnosis → OpenAI turns the diagnosis text into Hebrew.
  3. Both land in expo-sqlite on the device, so switching medium or language afterwards is instant and offline.
4Account & data The one path the Node server is not on.
  1. @supabase/supabase-js calls Supabase straight from the app: Auth, library rows, photo bucket.
  2. The session belongs to the user, so routing it through the server would only add a second thing to trust.
  3. Writes go to Supabase first, device second — a failed write never lands locally.
  4. The server reaches the same project with a service-role key, but only for the shared scrape cache.
Why one server and not five clients

Keys. Anything compiled into an Expo bundle is readable by anyone who downloads the app. Five provider keys on the device is five keys given away, and every credit spent on them is billed to the project.

Caching. A scrape paid for by one user is worth nothing if the next user cannot see it. The cache only works because every search passes through the same process, keyed the same way.

One thing to watch. GET /health answers for all five providers at once. Spread across clients, an outage is only visible in the reviews.