api / context /4_production.md
safraeli's picture
Deploy: 2026 sensor migration + redesign + bucket B endpoints
13fc29d verified
|
Raw History Blame Contribute Delete
22.5 kB
# 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:
```yaml
---
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)
1. **CORS** β€” allow `*.pages.dev` + localhost
2. **Rate limiter** β€” 60 req/min general, 10 req/min chatbot (Gemini costs)
3. **Auth** β€” JWT validation (initially optional, enforced in Phase 11.6)
4. **Request logging** β€” structured JSON logs
5. **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.
```typescript
// hooks/useWeather.ts
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, // 60s auto-refresh
});
}
```
```typescript
// lib/api.ts
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.
```yaml
# .github/workflows/control-tick.yml
name: Control Loop Tick
on:
schedule:
- cron: "*/15 * * * *" # every 15 min
workflow_dispatch: # manual trigger
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 }}
```
```yaml
# .github/workflows/daily-planner.yml
name: Day-Ahead Planner
on:
schedule:
- cron: "0 2 * * *" # 02:00 UTC = 05:00 IST
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)
```dockerfile
# backend/Dockerfile
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
# HuggingFace Spaces requires port 7860
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**.