SolarWine β Production Architecture
Overview
Migrate from a single-process Streamlit prototype to a 3-layer production system
deployed on free-tier infrastructure. The monorepo stays β no new repositories.
Baseline/ (monorepo)
βββ frontend/ NEW β Lovable-generated React app
β βββ src/
β β βββ pages/
β β β βββ Dashboard.tsx Overview + energy + weather
β β β βββ Advisor.tsx Chatbot + advisory cards
β β β βββ Control.tsx System status + tracker angles
β β β βββ Photosynthesis.tsx FvCB/ML results + forecasts
β β β βββ Shading.tsx Interactive simulator
β β β βββ Docs.tsx Documentation
β β βββ components/
β β β βββ EnergyCard.tsx
β β β βββ WeatherCard.tsx
β β β βββ Chatbot.tsx
β β β βββ TrackerStatus.tsx
β β β βββ Charts.tsx Recharts / Plotly wrappers
β β βββ hooks/
β β β βββ useWeather.ts fetch /api/weather/*
β β β βββ useEnergy.ts fetch /api/energy/*
β β β βββ useSensors.ts fetch /api/sensors/*
β β β βββ useChatbot.ts POST /api/chatbot/message
β β β βββ useControl.ts fetch /api/control/*
β β βββ lib/
β β β βββ api.ts API base URL + fetch wrapper
β β βββ App.tsx Router + layout
β βββ package.json
β βββ tailwind.config.js
β
βββ backend/ NEW β FastAPI middleware + background services
β βββ api/
β β βββ __init__.py
β β βββ main.py FastAPI app, CORS, lifespan
β β βββ routes/
β β β βββ weather.py GET /api/weather/*
β β β βββ sensors.py GET /api/sensors/*
β β β βββ energy.py GET /api/energy/*
β β β βββ photosynthesis.py GET /api/photosynthesis/*
β β β βββ control.py GET /api/control/*
β β β βββ chatbot.py POST /api/chatbot/message
β β β βββ health.py GET /api/health
β β βββ deps.py Shared dependencies (DataHub, Redis)
β β βββ auth.py JWT middleware (Phase 11.6)
β βββ workers/
β β βββ control_tick.py ControlLoop 15-min cron entry point
β β βββ daily_planner.py DayAheadPlanner daily cron entry point
β βββ Dockerfile
β βββ requirements.txt
β
βββ .github/
β βββ workflows/
β βββ control-tick.yml GitHub Actions: 15-min cron
β βββ daily-planner.yml GitHub Actions: daily 05:00 IST cron
β
βββ src/ UNCHANGED β shared Python core
β βββ control_loop.py
β βββ command_arbiter.py
β βββ tracker_dispatcher.py
β βββ day_ahead_planner.py
β βββ energy_budget.py
β βββ data/
β β βββ data_providers.py DataHub + 6 services
β β βββ thingsboard_client.py
β β βββ ims_client.py
β βββ chatbot/
β β βββ vineyard_chatbot.py
β β βββ guardrails.py
β β βββ routing_agent.py
β βββ models/
β β βββ farquhar_model.py
β β βββ phenology.py
β βββ forecasting/
β β βββ predictor.py
β β βββ ts_predictor.py
β βββ shading/
β β βββ solar_geometry.py
β β βββ tradeoff_engine.py
β βββ ...
β
βββ config/settings.py UNCHANGED β shared configuration
βββ app.py KEPT temporarily (Streamlit, deprecated)
βββ ui/ KEPT temporarily (Streamlit tabs, deprecated)
βββ docker-compose.yml NEW β local dev orchestration
βββ requirements.txt Shared Python deps
Layer 1 β The Brain (Backend Services)
What it is
All business logic, models, data access, and control algorithms. This layer
already exists in src/. The only change is extracting the runtime entry points
into backend/workers/.
Components
| Component |
Module |
Runtime |
Schedule |
| ControlLoop |
src/control_loop.py |
GitHub Actions cron |
Every 15 min |
| DayAheadPlanner |
src/day_ahead_planner.py |
GitHub Actions cron |
Daily 05:00 IST + re-plan events |
| DataHub |
src/data/data_providers.py |
Imported by API + workers |
Always available |
| ThingsBoard client |
src/data/thingsboard_client.py |
Imported by DataHub |
On-demand |
| IMS client |
src/data/ims_client.py |
Imported by DataHub |
On-demand (30-min TTL) |
| FvCB model |
src/models/farquhar_model.py |
Imported by PhotosynthesisService |
On-demand (~10ms) |
| ML ensemble |
src/forecasting/predictor.py |
Imported by PhotosynthesisService |
On-demand (~500ms) |
| Chronos forecaster |
src/forecasting/ts_predictor.py |
Imported by PhotosynthesisService |
On-demand (~2-5s) |
| CommandArbiter |
src/command_arbiter.py |
Imported by ControlLoop |
Per tick |
| TrackerDispatcher |
src/tracker_dispatcher.py |
Imported by ControlLoop |
Per tick |
| EnergyBudget |
src/energy_budget.py |
Imported by ControlLoop |
Per tick |
| Chatbot + guardrails |
src/chatbot/*.py |
Imported by API route |
Per message |
Caching strategy
| Data |
Current |
Production |
| IMS weather |
In-memory TTLCache (30 min) |
Upstash Redis (30 min TTL) |
| Vine sensors |
In-memory TTLCache (5 min) |
Upstash Redis (5 min TTL) |
| Energy daily |
No cache |
Upstash Redis (15 min TTL) |
| ML trained model |
Pickle file |
Pickle in /tmp + Redis metadata |
| Day-ahead plan |
JSON file |
Redis hash + JSON file backup |
| Chatbot history |
Streamlit session_state |
Redis list per session |
External dependencies
| Service |
URL |
Auth |
Purpose |
| ThingsBoard |
web.seymouragri.com |
Username/password β JWT |
21 sensors, 4 trackers, energy |
| IMS |
api.ims.gov.il |
API token |
Weather station 43 (Sde Boker) |
| Google Gemini |
generativelanguage.googleapis.com |
API key |
Chatbot (2.5-flash), routing |
| Upstash Redis |
*.upstash.io |
Token |
Shared cache |
Layer 2 β Middleware (FastAPI on HuggingFace Spaces)
What it is
A thin Python API layer that wraps DataHub services as REST endpoints. Hosted
on HuggingFace Spaces (Docker SDK, free tier) β Python-native, no cold starts
on the free tier's persistent container.
Tech stack
| Component |
Technology |
Why |
| Framework |
FastAPI |
Same Python team, async, auto-docs, WebSocket |
| Serialization |
Pydantic v2 |
Already a FastAPI dependency, typed responses |
| Auth |
JWT (PyJWT) |
Simple, stateless, no external IdP needed initially |
| Rate limiting |
slowapi |
Token bucket per IP, protects Gemini quota |
| CORS |
FastAPI CORSMiddleware |
Allow Cloudflare Pages frontend origin |
| WebSocket |
FastAPI WebSocket |
Live tracker angles, energy push (future) |
| Process manager |
uvicorn |
ASGI server, production-ready |
| Hosting |
HuggingFace Spaces (Docker SDK) |
Free, Python-native, persistent container |
HuggingFace Space configuration
The API Space uses Docker SDK. A Dockerfile at backend/ builds the FastAPI
server. The Space README metadata:
---
title: SolarWine API
emoji: πΏ
colorFrom: green
colorTo: yellow
sdk: docker
app_port: 7860
private: true
---
Note: HF Spaces exposes port 7860, so FastAPI binds to 0.0.0.0:7860.
API endpoints
Health
GET /api/health β { status, uptime, redis_ok, tb_ok }
Weather
GET /api/weather/current β WeatherService.get_current()
GET /api/weather/history?days=7 β WeatherService.get_history()
GET /api/weather/forecast β WeatherService.get_forecast()
Vine Sensors
GET /api/sensors/snapshot β VineSensorService.get_snapshot()
GET /api/sensors/history?type=crop&hours=24 β VineSensorService.get_history()
Energy
GET /api/energy/current β EnergyService.get_current()
GET /api/energy/daily/{date} β EnergyService.get_daily_production()
GET /api/energy/history?hours=24 β EnergyService.get_history()
GET /api/energy/predict/{date} β EnergyService.predict()
Photosynthesis
GET /api/photosynthesis/current β PhotosynthesisService.get_current()
GET /api/photosynthesis/forecast β PhotosynthesisService.forecast_day_ahead()
Control
GET /api/control/status β Last TickResult from Redis
GET /api/control/plan β Current DayAheadPlan from Redis
GET /api/control/budget β EnergyBudget current state
GET /api/control/trackers β Live tracker angles from TB
Chatbot
POST /api/chatbot/message β VineyardChatbot.chat()
Body: { message, session_id }
Response: { message, confidence, sources, caveats, rule_violations }
POST /api/chatbot/feedback β feedback.log_feedback()
Body: { session_id, message_id, rating, comment }
Biology
GET /api/biology/phenology β BiologyService current stage
GET /api/biology/rules β All biology rules
Middleware stack (order matters)
- CORS β allow
*.pages.dev + localhost
- Rate limiter β 60 req/min general, 10 req/min chatbot (Gemini costs)
- Auth β JWT validation (initially optional, enforced in Phase 11.6)
- Request logging β structured JSON logs
- Error handler β consistent error response format
Layer 3 β Frontend (Lovable β Cloudflare Pages)
What it is
Production-grade React frontend generated via Lovable (AI-powered React
builder). Produces a standard Vite + React + TypeScript + Tailwind project.
Deployed to Cloudflare Pages (free tier, unlimited bandwidth, private org repos supported).
Lovable generates the initial scaffold and components rapidly. After generation,
the code is committed to the monorepo under frontend/ and maintained manually
or with further Lovable iterations.
Tech stack
| Component |
Technology |
Why |
| Generator |
Lovable |
AI-generated React UI, rapid prototyping |
| Framework |
React 19 + Vite |
Lovable default output, fast build |
| Language |
TypeScript |
Type safety, IDE support |
| Styling |
Tailwind CSS + shadcn/ui |
Lovable default, beautiful components |
| Charts |
Recharts (default) or Plotly.js |
Data visualization |
| HTTP |
TanStack Query (React Query) |
Auto-refresh, caching, loading states |
| Routing |
React Router |
Page navigation |
| Hosting |
Cloudflare Pages (free) |
Unlimited bandwidth, private org repos, global CDN, auto-deploy from GitHub |
Pages (mapped from current Streamlit tabs)
| Streamlit Tab |
React Page |
Route |
Key Components |
| Vineyard Advisor |
Advisor.tsx |
/ |
Chatbot, weather card, energy card, shading card, irrigation card |
| System Status |
Control.tsx |
/control |
Tracker angles, energy budget gauge, control replay, ROI |
| Overview |
Dashboard.tsx |
/dashboard |
Energy history, crop comparisons |
| Photosynthesis & Data |
Photosynthesis.tsx |
/photosynthesis |
FvCB/ML results, sensor explorer |
| Forecasting |
β |
merged into /dashboard |
Weather + PS forecast cards |
| Shading Simulator |
Shading.tsx |
/shading |
Interactive offset tester |
| Documentation |
Docs.tsx |
/docs |
Help, API reference |
Data fetching pattern
Using TanStack Query for all API calls. Auto-refetch on window focus and
configurable polling intervals.
import { useQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
export function useCurrentWeather() {
return useQuery({
queryKey: ["weather", "current"],
queryFn: () => api.get("/api/weather/current"),
refetchInterval: 60_000,
});
}
const API_URL = import.meta.env.VITE_API_URL;
export const api = {
get: async (path: string) => {
const res = await fetch(`${API_URL}${path}`);
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
post: async (path: string, body: unknown) => {
const res = await fetch(`${API_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(res.statusText);
return res.json();
},
};
Frontend β Backend communication
| Pattern |
Use Case |
Protocol |
| REST |
All data reads, chatbot messages |
HTTPS (fetch via TanStack Query) |
| Polling |
Dashboard auto-refresh |
60s interval via refetchInterval |
| WebSocket |
Live tracker angles, energy (future) |
WSS (native WebSocket, future) |
Infrastructure & Deployment
Hosting (all free tier)
| Layer |
Platform |
Tier |
Limits |
| Frontend |
Cloudflare Pages |
Free |
Unlimited bandwidth, 500 builds/month, auto-deploy from GitHub |
| API server |
HuggingFace Spaces |
Free (Docker SDK) |
2 vCPU, 16GB RAM, persistent container, private spaces |
| Cron workers |
GitHub Actions |
Free (private repos) |
2,000 min/month, plenty for 15-min cron + daily planner |
| Cache |
Upstash Redis |
Free |
10K commands/day, 256MB, REST API |
| Secrets |
HF Space secrets + Cloudflare env vars + GitHub Actions secrets |
Free |
Per-service |
| Repo |
GitHub |
Free private |
Unlimited |
Deployment topology
GitHub (private repo, main branch)
β
βββββ push βββββΊ Cloudflare Pages (auto-deploy)
β βββ frontend/ β Vite build β static React app
β βββ URL: baseline.pages.dev (or custom domain)
β βββ Env: VITE_API_URL=https://<user>-solarwine-api.hf.space
β
βββββ push βββββΊ HuggingFace Space (auto-deploy via HF GitHub sync)
β βββ backend/ β Dockerfile β FastAPI (uvicorn, port 7860)
β βββ URL: https://<user>-solarwine-api.hf.space
β βββ Secrets: TB creds, IMS token, Gemini key, Redis URL
β
βββββ cron βββββΊ GitHub Actions (scheduled workflows)
βββ control-tick.yml: */15 * * * * β pip install + python control_tick.py
βββ daily-planner.yml: 0 2 * * * (02:00 UTC = 05:00 IST)
βββ Secrets: TB creds, IMS token, Redis URL
Upstash Redis
βββ URL: solarwine-cache-*.upstash.io (shared by API + workers)
GitHub Actions cron workflows
The ControlLoop and DayAheadPlanner run as scheduled GitHub Actions. This
replaces the need for a separate worker host.
name: Control Loop Tick
on:
schedule:
- cron: "*/15 * * * *"
workflow_dispatch:
jobs:
tick:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt -r backend/requirements.txt
- run: python -m backend.workers.control_tick
env:
THINGSBOARD_HOST: ${{ secrets.THINGSBOARD_HOST }}
THINGSBOARD_USERNAME: ${{ secrets.THINGSBOARD_USERNAME }}
THINGSBOARD_PASSWORD: ${{ secrets.THINGSBOARD_PASSWORD }}
IMS_API_TOKEN: ${{ secrets.IMS_API_TOKEN }}
UPSTASH_REDIS_URL: ${{ secrets.UPSTASH_REDIS_URL }}
UPSTASH_REDIS_TOKEN: ${{ secrets.UPSTASH_REDIS_TOKEN }}
name: Day-Ahead Planner
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- run: pip install -r requirements.txt -r backend/requirements.txt
- run: python -m backend.workers.daily_planner
env:
THINGSBOARD_HOST: ${{ secrets.THINGSBOARD_HOST }}
THINGSBOARD_USERNAME: ${{ secrets.THINGSBOARD_USERNAME }}
THINGSBOARD_PASSWORD: ${{ secrets.THINGSBOARD_PASSWORD }}
IMS_API_TOKEN: ${{ secrets.IMS_API_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
UPSTASH_REDIS_URL: ${{ secrets.UPSTASH_REDIS_URL }}
UPSTASH_REDIS_TOKEN: ${{ secrets.UPSTASH_REDIS_TOKEN }}
Docker setup (HuggingFace Space)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
COPY backend/requirements.txt backend/
RUN pip install -r requirements.txt -r backend/requirements.txt
COPY src/ src/
COPY config/ config/
COPY backend/ backend/
COPY Data/ Data/
ENV PYTHONPATH=/app
EXPOSE 7860
CMD ["uvicorn", "backend.api.main:app", "--host", "0.0.0.0", "--port", "7860"]
Environment variables
| Variable |
Where |
Source |
THINGSBOARD_HOST |
HF Space, GH Actions |
HF secrets, GH secrets |
THINGSBOARD_USERNAME |
HF Space, GH Actions |
HF secrets, GH secrets |
THINGSBOARD_PASSWORD |
HF Space, GH Actions |
HF secrets, GH secrets |
IMS_API_TOKEN |
HF Space, GH Actions |
HF secrets, GH secrets |
GOOGLE_API_KEY |
HF Space, GH Actions |
HF secrets, GH secrets |
UPSTASH_REDIS_URL |
HF Space, GH Actions |
HF secrets, GH secrets |
UPSTASH_REDIS_TOKEN |
HF Space, GH Actions |
HF secrets, GH secrets |
JWT_SECRET |
HF Space |
HF secrets |
ALLOWED_ORIGINS |
HF Space |
HF secrets (Cloudflare Pages URL) |
VITE_API_URL |
Cloudflare Pages |
Cloudflare env vars (HF Space URL) |
Data Flow
Request flow (user views dashboard)
Browser β Cloudflare CDN β React SPA (static)
β
βββ useQuery β fetch https://<user>-solarwine-api.hf.space/api/weather/current
β
βΌ
FastAPI route (HuggingFace Spaces)
β
βΌ
DataHub.weather.get_current()
β
βββ Redis cache hit? β return cached
βββ Redis miss β IMS API β cache β return
Control flow (15-min tick)
GitHub Actions cron (*/15 * * * *)
β
βββ python -m backend.workers.control_tick
β
βΌ
ControlLoop.tick()
β
βββ DataHub.weather.get_current() β IMS (via Redis cache)
βββ DataHub.vine_sensors.get_snapshot() β ThingsBoard
βββ RoutingAgent β FvCB or ML model
βββ DayAheadPlan lookup β Redis
βββ InterventionGate check
βββ BudgetGuard check
βββ CommandArbiter β decide angle
βββ TrackerDispatcher β ThingsBoard shared attributes
βββ EnergyBudget.spend_slot()
βββ Save TickResult β Redis (for API /control/status)
Chatbot flow
Browser β React β POST /api/chatbot/message { message, session_id }
β
βΌ
FastAPI route (HF Space) β VineyardChatbot.chat(message, hub=DataHub)
β
βββ QueryClassifier β data / knowledge / greeting
βββ Gemini Pass 1 β tool calls (if data query)
βββ DataHub.dispatch_tool() β real data
βββ tag_tool_result() β source metadata
βββ Gemini Pass 2 β grounded response
βββ ResponseValidator β rule checks
βββ estimate_confidence()
βββ Return ChatResponse β JSON β Browser
Security
| Concern |
Mitigation |
| API keys in repo |
.env in .gitignore; secrets in HF/Cloudflare/GH settings |
| TB credentials |
Server-side only; frontend never sees TB creds |
| Gemini API abuse |
Rate limit 10 req/min on /api/chatbot/message |
| CORS |
Whitelist Cloudflare Pages domain only |
| Auth (Phase 11.6) |
JWT tokens; initially optional for research use |
| Tracker commands |
Only GH Actions worker can write to TB; API is read-only |
| HF Space |
Set to private β not publicly accessible without auth |
Migration from Streamlit
The existing app.py and ui/ folder remain functional throughout migration.
They are deprecated once the Lovable frontend reaches feature parity.
| Phase |
Streamlit |
Lovable/React |
Notes |
| 11.1β11.3 |
Primary |
Not started |
Backend being built |
| 11.4 |
Primary |
Scaffold |
Pages generated via Lovable |
| 11.5 |
Deprecated |
Primary |
Feature parity reached |
| 11.6+ |
Removed |
Primary |
Auth, monitoring, polish |
Scaling path (when free tier is outgrown)
| Bottleneck |
Upgrade |
Cost |
| HF Space resources |
HF Space upgrade (2Γ CPU, 16GB) |
$0 (already free) β $9/mo for GPU |
| GitHub Actions minutes |
GitHub Team (3,000 min) |
$4/user/mo |
| Upstash 10K commands/day |
Upstash Pay-as-you-go ($0.20/100K) |
~$1/mo |
| Cloudflare Pages |
Already unlimited on free tier |
$0 |
| Heavy ML inference |
Add GPU worker (Modal/RunPod) |
$5β10/mo |
| Multi-site (>1 vineyard) |
Add Postgres (Neon free) for multi-tenant state |
Free β $19/mo |
Total cost to run a real commercial deployment: ~$25/month.