SmartPot-API-Go is the core of SmartPot rewritten in Go 1.26 with Echo: a REST and MQTT API that connects hydroponic crops, real or virtual, with the web application. It receives telemetry via MQTT, persists it in PostgreSQL + TimescaleDB, sends commands to actuators, manages device credentials on the broker, and queries the AI assistant to diagnose each crop and, when permitted by the user, act autonomously.
It provides a seamless migration from the Spring Boot version: it preserves all external contracts — REST routes, JSON payloads, error handling, MQTT topics and broker provisioning, cache keys, SMTP, and HTTP calls to AI, simulator, and Telegram — ensuring the web client, broker, hardware devices, AI, and simulator operate without disruption. The database is an internal detail behind a port: the API is the control plane and the persistence engine (PostgreSQL + TimescaleDB) can be swapped without touching the business domain. The cache follows the same pattern: behind the Cache port is an adapter (Valkey, the open-source Redis replacement) that can be swapped without altering services. Existing JWT tokens and device keys remain valid (same HS256 secret, cost-12 BCrypt, and AES-256-GCM). Internally it leverages Go concurrency: the MQTT client, Telegram bot, scheduled workers, and in-memory event bus run in parallel without thread pools or cron daemons.
flowchart LR
web["SmartPot-Web<br/>PWA"] -->|"REST + JWT"| api["SmartPot-API-Go<br/>:8091"]
esp["Real crop<br/>ESP32 or Wokwi"] -->|"MQTT"| broker["SmartPot-Broker"]
broker <-->|"telemetry · commands · ACK"| api
api --> db[("PostgreSQL<br/>+ TimescaleDB")]
api --> cache[("Valkey")]
api --> mail["Mailpit / SMTP"]
api -->|"evaluation · continual learning"| ai["SmartPot-AI"]
api -->|"virtual crops & weather"| sim["SmartPot-DataGenerator"]
sim -->|"MQTT"| broker
api -->|"notifications & digests"| tg["Telegram"]
classDef core fill:#067A52,stroke:#06281C,color:#fff;
classDef ext fill:#DDF5EA,stroke:#067A52,color:#17261F;
class api core;
class web,esp,broker,db,cache,mail,ai,sim,tg ext;
Code is organized inside internal/, one package per domain. Each domain packages handler.go, service.go, model.go, dto.go, and port.go — the Repository interface required by the service from the data layer; implementations reside separately in internal/storage/postgres. Thus domain logic remains unaware of database specifics (hexagonal architecture / ports and adapters). The cache layer follows the same standard: services depend on cache.Cache and external access is handled via cache.Backend implemented in internal/cache/valkey. cmd/api serves as the entry point wiring components and launching the HTTP/MQTT listeners.
cmd/api/ # Entry point: configuration loading, dependency wiring, and runtime bootstrap
internal/
├── config/ # Environment loading and ISO-8601 duration parser (PT5M, P7D)
├── httpserver/ # Echo engine, typed RequestContext (sync.Pool), decoupled validator, centralized HTTPErrorHandler
├── security/ # JWT HS256, BCrypt, AES-GCM, CORS, rate limiter, and typed guards
├── cache/ # Cache + Backend ports and resilient in-memory fallback store
├── cache/valkey/ # Valkey (open Redis replacement) Backend adapter via go-redis
├── storage/postgres/ # Repository adapters for PostgreSQL + TimescaleDB (pool and SQL)
├── mqtt/ # Paho client, v1 topics, telemetry parser, and Mosquitto/EMQX provisioning
├── mail/ # Welcome and password reset notifications
├── openapi/ # OpenAPI 3 document at /v3/api-docs and Swagger UI at /docs
├── platform/ # Cross-cutting concerns: event bus, validation, ObjectId, geo, and logging
├── actuators/ # Pump, UV light, fan, humidifier, and doser management
├── ai/ # AI service client, health evaluation, autonomous agent, and continual learning
├── auth/ # Registration, authentication, and password recovery
├── channels/ # External notification channels (Telegram) and interactive bot
├── commands/ # MQTT commands: PENDING → SENT → EXECUTED | FAILED | EXPIRED
├── crops/ # Real and virtual crops, cultivation form, automation mode, and credentials
├── health/ # /health probes for orchestrators and load balancers
├── notifications/ # Crop alerts, device status, and assistant activity notices
├── overview/ # Dashboard: aggregate statistics, comparative time series, and fleet analysis
├── readings/ # Telemetry readings, statistical summaries, and CSV export
├── users/ # Account profile, password updates, and account deletion
└── virtualdevices/ # Virtual crop simulation: configuration, pausing, and simulator proxy
Key HTTP layer architecture decisions (internal/httpserver & internal/security):
- Compile-time Typed Context (
*httpserver.RequestContext). Each HTTP request wraps the raw context in a pooledRequestContext(sync.Pool) providing typed fields for identity (Identity) and tracing (TelemetryTrace), eliminating stringly typed lookups (c.Get/c.Set). - Explicit Error Returns and Centralized
HTTPErrorHandler. Handlers return idiomaticerror(*apperr.Erroror*apperr.ValidationError), processed centrally into standardized JSON responses. - Linear Middleware Composition. Functional pipeline
func(next echo.HandlerFunc) echo.HandlerFuncfor logging, panic recovery, security headers, CORS, rate limiting, and auth guards. - Zero-Allocation Radix Router. Route resolution and parameter extraction run on Echo's radix tree without dynamic heap allocations.
- Interchangeable Validation (
echo.Validator). Decoupled validation interface delegating toplatform/validation.
Local base http://localhost:8091, production https://api.smartpot.app. Business routes reside under /api/v1 and require Authorization: Bearer <token>. Interactive Swagger UI is hosted at /docs and the OpenAPI spec at /v3/api-docs.
| Method | Route | Access |
|---|---|---|
| GET | /health → {"status","database","broker","cache","ai","simulator"} (503 on database down) |
Public |
| POST | /api/v1/auth/register {name,lastName,email,password} → {token,expiresAt,user} |
Public |
| POST | /api/v1/auth/login {email,password} |
Public |
| POST | /api/v1/auth/password/forgot {email} → 202 |
Public |
| POST | /api/v1/auth/password/reset {token,password} → 204 |
Public |
| GET | /api/v1/crop-profiles (optimal ranges per crop species) |
Public |
| GET PUT DELETE | /api/v1/users/me (PUT {name,lastName}) |
JWT |
| PUT | /api/v1/users/me/password {currentPassword,newPassword} |
JWT |
| GET POST | /api/v1/crops (POST {name,type,kind,form,placement?,virtual?}; returns device key once for real crops) |
JWT |
| PUT | /api/v1/crops/automation {cropIds?,enabled} (bulk automation toggle) |
JWT |
| GET PUT DELETE | /api/v1/crops/{id} (PUT {name,type,form?,placement?}) |
JWT, owner only |
| PUT | /api/v1/crops/{id}/automation {enabled} |
JWT, owner only |
| GET | /api/v1/crops/{id}/weather (location weather; 204 if unlocated or unavailable) |
JWT, owner only |
| GET | /api/v1/crops/{id}/device (broker host, username, and topic prefixes; 204 for virtual crops) |
JWT, owner only |
| POST | /api/v1/crops/{id}/device/key (rotates key and disconnects active broker session; real crops only) |
JWT, owner only |
| GET POST | /api/v1/crops/{id}/readings (from, to, limit) |
JWT, owner only |
| GET | /api/v1/crops/{id}/readings/latest, /summary?hours=24, /export (CSV) |
JWT, owner only |
| GET POST DELETE | /api/v1/crops/{id}/actuators, /actuators/{actuatorId} |
JWT, owner only |
| GET POST | /api/v1/crops/{id}/commands (POST {actuatorId,action,durationSeconds} → 202; 409 on state conflict) |
JWT, owner only |
| GET | /api/v1/crops/{id}/insights (AI assistant diagnosis and predictions) |
JWT, owner only |
| GET | /api/v1/overview (account totals and crops with latest telemetry) |
JWT |
| GET | /api/v1/overview/series?metric=temperature&hours=24 (comparative interval averages) |
JWT |
| GET | /api/v1/overview/fleet (fleet ranking, shared issues, clusters, and bulk recommendations) |
JWT |
| GET | /api/v1/commands?limit=50 (command history across all crops) |
JWT |
| POST | /api/v1/commands/bulk {cropIds?,actuatorType,action,durationSeconds} → 202 per crop status (SENT, SKIPPED, FAILED) |
JWT |
| GET | /api/v1/notifications (unreadOnly, limit), /unread-count |
JWT |
| PUT DELETE | /api/v1/notifications/{id}/read, /read-all, /{id} |
JWT |
| GET | /api/v1/channels (configured notification channels and links) |
JWT |
| POST | /api/v1/channels/telegram/link → {code,url,expiresAt} (single-use linking code, 10 minutes) |
JWT |
| PUT POST DELETE | /api/v1/channels/links/{id} {enabled?,events?}, /links/{id}/test, /links/{id} |
JWT, owner only |
| POST | /api/v1/channels/telegram/webhook (webhook mode, authenticated with X-Telegram-Bot-Api-Secret-Token) |
Telegram |
| GET PUT | /api/v1/crops/{id}/channels, /channels/{type} {enabled?,events?,delivery?,digestHours?,dailySummaryAt?} |
JWT, owner only |
| POST DELETE | /api/v1/crops/{id}/channels/{type}/recipients → {code,url,expiresAt} (chat sharing link), /recipients/{recipientId} |
JWT, owner only |
| GET PUT DELETE | /api/v1/crops/{id}/virtual-device (virtual crops only; PUT updates mode, DELETE pauses) |
JWT, owner only |
| GET | /api/v1/virtual-devices/places?q= (weather location search) |
JWT |
| GET | /api/v1/ai/learning (continual learning model status: aggregated species metrics) |
JWT |
- Supported Species:
TOMATO,LETTUCE,STRAWBERRY,BASIL,SPINACH,PEPPER. - Crop Kind:
REALorVIRTUAL, immutable upon creation (REALby default; 400 on modification attempt). - Cultivation Form:
POT,NFT,TOWER,RAFT(POTby default; updates UI illustration). - Placement:
settingINDOORorOUTDOOR,exposureFULL_SUN,PARTIAL_SUN, orSHADE, plus location. - Actuator Types:
WATER_PUMP,UV_LIGHT,FAN,HUMIDIFIER,NUTRIENT_DOSER,PH_DOSER. Real crops initialize with pump, UV light, and fan; virtual crops initialize with all six. - Password Policy: 8 characters to 72 bytes requiring uppercase, lowercase, and numbers (BCrypt cost 12).
- Privacy: Queries on crops belonging to other accounts return 404, preventing ID enumeration.
- Rate Limits: 300 requests/min per IP, 10 requests/min for
/api/v1/auth/*(429 withRetry-After).
The message broker is SmartPot-Broker-EMQX, delegating device authentication to the API (POST /internal/mqtt/authn using shared secret MQTT_AUTHN_TOKEN). The API validates the crop device key (AES-256-GCM encrypted) and returns an ACL constrained to its specific topics. The verification cache stores an HMAC of the key. Devices connect with username = crop ID and password = device key; rotating or deleting keys terminates active EMQX sessions.
| Topic | Direction | Payload |
|---|---|---|
smartpot/v1/{cropId}/telemetry |
Device → API | {"temperature":24.5,"humidity":61,"brightness":710,"ph":6.1,"tds":820,"atmosphere":1012.8,"soilMoisture":55} |
smartpot/v1/{cropId}/commands |
API → Device | {"id":"…","actuator":"WATER_PUMP","action":"ACTIVATE","durationSeconds":30} |
smartpot/v1/{cropId}/commands/ack |
Device → API | {"id":"…","status":"EXECUTED","message":"Water pump active for 15 s"} |
smartpot/v1/{cropId}/status |
Device (retained / LWT) | online / offline |
Telemetry can also arrive as compact binary v2 packets (27 bytes, same topic) containing sensor presence bitmasks, 32-bit light, and pressure fields (internal/mqtt/parser.go). Telemetry outside valid physical ranges is dropped; each crop records at most one sample per 5 seconds. Commands without an ACK within 2 minutes transition to EXPIRED.
Incoming readings trigger the autonomous agent, evaluating crops every 30 seconds when automation is enabled (every 5 minutes otherwise). Readings are forwarded to SmartPot-AI alongside historical series and actuator states. The AI service responds with a fuzzy health index, expert rule diagnoses, learned model predictions, and recommended actions. The API persists health status, issues notifications for critical conditions, and under automation mode executes actions with a 10-minute actuator cooldown.
Readings from real crops are buffered for continual learning: every minute the API delivers batches to the AI engine (/v1/learning/readings) with local hour context. Fleet insights aggregate time-series data with TimescaleDB time_bucket to generate cross-crop rankings and collective actions.
PWA alerts and notifications are forwarded to linked external channels. Channels implement NotificationChannel, allowing modular addition of Slack or WhatsApp integrations. Currently Telegram is supported natively:
- The PWA requests a linking token (
POST /api/v1/channels/telegram/link) and openshttps://t.me/<bot>?start=<code>. - The bot parses
/start <code>, validates the one-time code, and associates the chat with the user's account. - Interactive
/statuslists crops, and/unlinkdetaches the chat.
In development TELEGRAM_MODE=polling runs long-polling; production uses webhook with signature verification.
Users can generate personal connection tokens (spt_...) from the PWA (Profile › Connections) to interface with external AI agents such as SmartPot-MCP: GET/POST /api/v1/connections and DELETE /api/v1/connections/{id}. Tokens are revealed once upon creation; hashes are stored securely.
| Scope | Permissions |
|---|---|
READ |
Read-only queries (GET) |
CONTROL |
Create, update, delete crops and trigger actuators |
Connection tokens are strictly blocked from changing user account settings, managing other tokens, viewing device credentials, or accessing admin operations.
Crops specify their data origin upon creation:
- Real Crops generate a unique device key for ESP32 firmware running on hardware or simulated via Wokwi.
- Virtual Crops require no hardware: SmartPot-DataGenerator executes continuous simulations using internal credentials. Virtual crops can be paused, resumed, and configured with manual or weather-based environmental profiles (up to 5 virtual crops per user).
The container image is publicly available at ghcr.io/smartpottech/smartpot-api-go. The full platform can be started with a single command without downloading source code:
curl -fsSL https://raw.githubusercontent.com/SmartPotTech/.github/main/docker/demo-beta/compose.yaml | docker compose -f - up -d --waitAccess http://localhost:5173 with credentials demo@smartpot.app / SmartPot2026. Stop with docker compose -p smartpot-demo-beta down (add -v to wipe data). See the demo beta guide.
- Go 1.26 (or Docker)
- PostgreSQL + TimescaleDB (SmartPot-DB-Postgre)
- Valkey (SmartPot-Cache-Valkey)
- Mailpit, MQTT broker, and AI service: complete stack available in SmartPotTech/.github
cp .env.example .env # Configure secrets and connection parameters
go run ./cmd/apiSMARTPOT_JWT_SECRET (at least 32 characters) and SMARTPOT_AES_KEY (32-byte Base64 key) are mandatory.
go test ./...The test suite covers encryption/decryption parity, JWT handling, password policies, telemetry ranges, MQTT topics and provisioning, command timeouts, actuator switches (409 conflict detection), agricultural thresholds, agent rules, continual learning pipelines, Telegram bot handlers, OpenAPI schemas, and caching behaviors.
docker build -t smartpot-api-go .
docker pull ghcr.io/smartpottech/smartpot-api-go:latestThe Dockerfile builds a statically linked binary copied into a minimal scratch image (no shell, package manager, or network binaries). Runs under UID 1000 with a read-only root filesystem and embedded healthcheck command.
- OpenAPI interactive Swagger UI:
/docs - OpenAPI JSON specification:
/v3/api-docs - Platform architecture & protocol docs: Platform Documentation
Global platform diagrams:
- gRPC AI Service with REST Fallback. Configured via
AI_GRPC_SOCKET(UNIX domain socket) orAI_GRPC_ADDR(HTTP/2 internal TCP). Falls back seamlessly to REST HTTP/1.1 if gRPC is unavailable. - Server-Sent Events (SSE).
GET /api/v1/crops/{cropId}/streamdelivers real-time telemetry events (X-Accel-Buffering: nocompatible). - Batch Telemetry Ingestion. MQTT telemetry is ingested in batches of up to 100 readings or every 10 ms using PostgreSQL binary
COPY. - Deadband Filtering. The autonomous agent avoids redundant AI evaluations if environmental variations remain below deadband thresholds (0.1 °C, 0.4% RH, 0.03 pH, 15 ppm, 0.5% moisture, 0.5 hPa, 5% light).
- Reverse Proxy Support. When
TRUST_PROXY=true, client IP addresses are resolved fromX-Forwarded-Forstrictly against configuredTRUSTED_PROXY_CIDRS.
This project is licensed under the MIT License. See LICENSE for details.
SmartPot-API-Go es el núcleo de SmartPot reescrito en Go 1.26 con Echo: una API REST y MQTT que conecta los cultivos hidropónicos, reales o virtuales, con la aplicación web. Recibe la telemetría por MQTT, la guarda en PostgreSQL + TimescaleDB, envía comandos a los actuadores, administra las credenciales de cada dispositivo en el broker y consulta al asistente de IA para diagnosticar cada cultivo y, si el usuario lo permite, actuar por su cuenta.
Es una migración transparente de la versión en Spring Boot: reproduce idénticos todos los contratos externos —rutas
REST, formas JSON, códigos y mensajes en español, tópicos MQTT y aprovisionamiento en Mosquitto, claves de la caché, SMTP y
las llamadas HTTP a la IA, al simulador y a Telegram—, de modo que la web, el broker, los dispositivos, la IA y el
simulador siguen funcionando sin cambios. La base de datos es un detalle interno detrás de un puerto: la API es el
centro de control y el motor (hoy PostgreSQL + TimescaleDB, antes MongoDB) se puede cambiar sin tocar el dominio.
La caché sigue el mismo patrón: detrás del puerto Cache hay un adaptador (hoy Valkey, el reemplazo libre de
Redis) que se puede cambiar sin tocar los servicios.
Los tokens JWT y las claves de dispositivo ya
emitidos siguen siendo válidos (mismo secreto HS256, BCrypt de costo 12 y AES-256-GCM). Por dentro aprovecha las
goroutines: el cliente MQTT, el bot de Telegram, los trabajos periódicos y el bus de eventos corren en paralelo sin
hilos ni cron en el servidor.
flowchart LR
web["SmartPot-Web<br/>PWA"] -->|"REST + JWT"| api["SmartPot-API-Go<br/>:8091"]
esp["Cultivo real<br/>ESP32 o Wokwi"] -->|"MQTT"| broker["SmartPot-Broker"]
broker <-->|"telemetría · comandos · ACK"| api
api --> db[("PostgreSQL<br/>+ TimescaleDB")]
api --> cache[("Valkey")]
api --> mail["Mailpit / SMTP"]
api -->|"evaluación · lecturas para aprender"| ai["SmartPot-AI"]
api -->|"cultivos virtuales y clima"| sim["SmartPot-DataGenerator"]
sim -->|"MQTT"| broker
api -->|"avisos y resúmenes"| tg["Telegram"]
classDef core fill:#067A52,stroke:#06281C,color:#fff;
classDef ext fill:#DDF5EA,stroke:#067A52,color:#17261F;
class api core;
class web,esp,broker,db,cache,mail,ai,sim,tg ext;
El código vive en internal/, un paquete por dominio (los mismos de la versión en Java, para rastrear la
equivalencia). Cada dominio reúne handler.go, service.go, model.go, dto.go y port.go —la interfaz
Repository que el servicio necesita de la capa de datos—; las implementaciones de esos puertos sobre PostgreSQL
viven aparte, en internal/storage/postgres. Así el dominio no conoce el motor de base de datos (inversión de
dependencias / puertos y adaptadores). La caché aplica el mismo patrón: los servicios dependen del puerto cache.Cache
y el acceso al sistema remoto está detrás del puerto de salida cache.Backend, cuyo adaptador vive en
internal/cache/valkey; cambiar de sistema de caché es solo otro adaptador. cmd/api es el punto de entrada que
cablea todo y arranca el servidor.
cmd/api/ # Punto de entrada: carga la config, cablea los componentes y arranca
internal/
├── config/ # Carga del entorno y parser de duraciones ISO-8601 (PT5M, P7D)
├── httpserver/ # Motor Echo, RequestContext tipado (sync.Pool), validador desacoplado y HTTPErrorHandler central
├── security/ # JWT HS256, BCrypt, AES-GCM, CORS, límite de peticiones y guardas tipadas
├── cache/ # Puerto Cache + Backend y almacén resiliente con respaldo en memoria (contadores y enfriamientos)
├── cache/valkey/ # Adaptador del puerto Backend sobre Valkey (reemplazo libre de Redis) vía go-redis
├── storage/postgres/ # Adaptadores de los puertos Repository sobre PostgreSQL + TimescaleDB (pool y SQL)
├── mqtt/ # Cliente Paho, tópicos v1, parser de telemetría y aprovisionamiento en Mosquitto
├── mail/ # Correos de bienvenida y de recuperación de contraseña
├── openapi/ # Documento OpenAPI en /v3/api-docs e interfaz de Swagger en /docs
├── platform/ # Piezas transversales: bus de eventos, validación, ObjectId, geo y logging
├── actuators/ # Bomba, luz ultravioleta, ventilador y demás salidas de cada cultivo
├── ai/ # Cliente del servicio de IA, evaluación de salud, agente y aprendizaje continuo
├── auth/ # Registro, inicio de sesión y recuperación de contraseña
├── channels/ # Canales externos de notificación (hoy Telegram) y su bot
├── commands/ # Comandos por MQTT: PENDING → SENT → EXECUTED | FAILED | EXPIRED
├── crops/ # Cultivos reales o virtuales, su forma, modo automático y credenciales
├── health/ # /health para despliegues y balanceadores
├── notifications/ # Alertas del cultivo, del dispositivo y del asistente
├── overview/ # Panel general: totales, series comparativas y análisis de flota de la IA
├── readings/ # Lecturas, resumen estadístico y exportación CSV
├── users/ # Perfil, cambio de contraseña y borrado de cuenta
└── virtualdevices/ # Simulación de los cultivos virtuales: configuración, pausa y proxy al simulador
Spring aporta beans, @Scheduled, eventos y validación declarativa; aquí eso se traduce en: cableado explícito en
cmd/api, goroutines con time.Ticker canceladas por context al apagar, un bus de eventos en proceso
(platform/events) y un validador propio con los mismos mensajes en español.
La capa HTTP (internal/httpserver e internal/security) se apoya en cinco decisiones de diseño propias de Echo:
- Contexto tipado en tiempo de compilación (
*httpserver.RequestContext). Comoecho.Contextes una interfaz desacoplada, cada petición envuelve el contexto base en unRequestContextreciclado mediantesync.Poolque expone campos tipados para la identidad del usuario, las credenciales JWT (Identity) y la traza de telemetría (TelemetryTrace), sin mapas de cadenas (c.Set/c.Get) ni conversiones de tipo en tiempo de ejecución. - Retorno explícito de error y
HTTPErrorHandlercentralizado. Todos los manejadores usan la firma idiomáticafunc(echo.Context) errory devuelven directamente los errores de dominio (*apperr.Error) o de validación (*apperr.ValidationError) para que elHTTPErrorHandlercentral los transforme en respuestas JSON homogéneas sin riesgo de ejecuciones residuales. - Composición lineal de middlewares. La tubería HTTP (
requestLogger,recoverer, cabeceras de seguridad, CORS, límite de tasa yGuard) se construye por composición funcionalfunc(next echo.HandlerFunc) echo.HandlerFunc. - Árbol Radix zero-allocation. La resolución de rutas y la extracción de parámetros (
c.Param) operan sobre el árbol Radix de Echo sin asignaciones dinámicas en el heap (0 allocs/opverificado en pruebas), aplanando el impacto del recolector de basura bajo concurrencia continua. - Validación intercambiable (
echo.Validator).httpserver.Binddelega enc.Validate(req)a través de la interfazecho.Validator, manteniendo las reglas deplatform/validationdesacopladas del transporte y sin dependencias externas de reflexión.
Base local http://localhost:8091, producción https://api.smartpot.app. Las rutas de negocio viven bajo /api/v1 y
usan Authorization: Bearer <token>. La documentación interactiva está en /docs y el esquema en /v3/api-docs.
| Método | Ruta | Acceso |
|---|---|---|
| GET | /health → {"status","database","broker","cache","ai","simulator"} (503 si la base cae) |
Público |
| POST | /api/v1/auth/register {name,lastName,email,password} → {token,expiresAt,user} |
Público |
| POST | /api/v1/auth/login {email,password} |
Público |
| POST | /api/v1/auth/password/forgot {email} → 202 siempre |
Público |
| POST | /api/v1/auth/password/reset {token,password} → 204 |
Público |
| GET | /api/v1/crop-profiles (rangos óptimos por especie) |
Público |
| GET PUT DELETE | /api/v1/users/me (PUT {name,lastName}) |
JWT |
| PUT | /api/v1/users/me/password {currentPassword,newPassword} |
JWT |
| GET POST | /api/v1/crops (POST {name,type,kind,form,placement?,virtual?}; si es real devuelve la clave del dispositivo una sola vez) |
JWT |
| PUT | /api/v1/crops/automation {cropIds?,enabled} (modo automático en varios cultivos; sin ids, en todos) |
JWT |
| GET PUT DELETE | /api/v1/crops/{id} (PUT {name,type,form?,placement?}) |
JWT, solo el dueño |
| PUT | /api/v1/crops/{id}/automation {enabled} |
JWT, solo el dueño |
| GET | /api/v1/crops/{id}/weather (clima del lugar; 204 sin ubicación o sin clima disponible) |
JWT, solo el dueño |
| GET | /api/v1/crops/{id}/device (broker, usuario y tópicos; 204 si no es real) |
JWT, solo el dueño |
| POST | /api/v1/crops/{id}/device/key (rota la clave y desconecta la sesión anterior; solo cultivos reales) |
JWT, solo el dueño |
| GET POST | /api/v1/crops/{id}/readings (from, to, limit) |
JWT, solo el dueño |
| GET | /api/v1/crops/{id}/readings/latest, /summary?hours=24, /export (CSV) |
JWT, solo el dueño |
| GET POST DELETE | /api/v1/crops/{id}/actuators, /actuators/{actuatorId} |
JWT, solo el dueño |
| GET POST | /api/v1/crops/{id}/commands (POST {actuatorId,action,durationSeconds} → 202; 409 si no cambia nada o hay otra orden en curso) |
JWT, solo el dueño |
| GET | /api/v1/crops/{id}/insights (diagnóstico del asistente de IA con pronósticos) |
JWT, solo el dueño |
| GET | /api/v1/overview (totales de la cuenta y cada cultivo con su última lectura) |
JWT |
| GET | /api/v1/overview/series?metric=temperature&hours=24 (promedios por intervalo para comparar cultivos) |
JWT |
| GET | /api/v1/overview/fleet (ranking, problemas compartidos, grupos y acciones en bloque de la IA) |
JWT |
| GET | /api/v1/commands?limit=50 (comandos de todos los cultivos) |
JWT |
| POST | /api/v1/commands/bulk {cropIds?,actuatorType,action,durationSeconds} → 202 con el resultado por cultivo (SENT, SKIPPED con su motivo o FAILED) |
JWT |
| GET | /api/v1/notifications (unreadOnly, limit), /unread-count |
JWT |
| PUT DELETE | /api/v1/notifications/{id}/read, /read-all, /{id} |
JWT |
| GET | /api/v1/channels (canales del servidor y mis vínculos) |
JWT |
| POST | /api/v1/channels/telegram/link → {code,url,expiresAt} (código de un solo uso, 10 minutos) |
JWT |
| PUT POST DELETE | /api/v1/channels/links/{id} {enabled?,events?}, /links/{id}/test, /links/{id} |
JWT, solo el dueño |
| POST | /api/v1/channels/telegram/webhook (modo webhook, firmado con X-Telegram-Bot-Api-Secret-Token) |
Telegram |
| GET PUT | /api/v1/crops/{id}/channels, /channels/{type} {enabled?,events?,delivery?,digestHours?,dailySummaryAt?} (avisos del cultivo) |
JWT, solo el dueño |
| POST DELETE | /api/v1/crops/{id}/channels/{type}/recipients → {code,url,expiresAt} (compartir con otro chat), /recipients/{recipientId} |
JWT, solo el dueño |
| GET PUT DELETE | /api/v1/crops/{id}/virtual-device (solo cultivos virtuales; PUT {mode,manual?,location?,intervalSeconds?} cambia o reanuda, DELETE pausa) |
JWT, solo el dueño |
| GET | /api/v1/virtual-devices/places?q= (lugares para el modo clima) |
JWT |
| GET | /api/v1/ai/learning (qué ha aprendido la IA: solo datos agregados por especie) |
JWT |
- Especies:
TOMATO,LETTUCE,STRAWBERRY,BASIL,SPINACH,PEPPER. - Tipo (
kind):REALoVIRTUAL, fijo al crear el cultivo (REALsi falta); cambiarlo responde 400. - Forma (
form):POT,NFT,TOWERoRAFT(POTsi falta); se puede editar y solo cambia la ilustración de la PWA. - Lugar (
placement):settingINDOORuOUTDOOR,exposureFULL_SUN,PARTIAL_SUNoSHADEylocation; todo opcional y uno solo para el cultivo y su simulación. - Actuadores:
WATER_PUMP,UV_LIGHT,FAN,HUMIDIFIER,NUTRIENT_DOSER,PH_DOSER. Un cultivo real nace con la bomba, la luz ultravioleta y el ventilador; uno virtual, con los seis. Funcionan como switches: admiten una orden a la vez, apagar lo apagado o encender sin límite lo encendido responde 409, y cada uno informarunningyrunningUntil. - Contraseñas de 8 caracteres a 72 bytes con mayúscula, minúscula y número (BCrypt de costo 12).
- Un cultivo de otra cuenta responde 404, igual que uno inexistente, para no revelar qué ids existen.
- Límite por IP: 300 peticiones por minuto y 10 por minuto en
/api/v1/auth/*. Responde 429 conRetry-After.
El broker es SmartPot-Broker-EMQX, que delega la autenticación
de cada dispositivo en la API (POST /internal/mqtt/authn, con el secreto compartido MQTT_AUTHN_TOKEN): la API
verifica la clave del cultivo (guardada cifrada con AES-256-GCM) y responde un ACL acotado a sus tópicos. La caché de
esa verificación solo guarda un HMAC de la clave. El dispositivo se conecta con usuario = id del cultivo y la
clave del dispositivo; al rotar o borrar la clave, la API corta sus sesiones en EMQX.
| Tópico | Sentido | Carga |
|---|---|---|
smartpot/v1/{cropId}/telemetry |
Dispositivo → API | {"temperature":24.5,"humidity":61,"brightness":710,"ph":6.1,"tds":820,"atmosphere":1012.8,"soilMoisture":55} |
smartpot/v1/{cropId}/commands |
API → dispositivo | {"id":"…","actuator":"WATER_PUMP","action":"ACTIVATE","durationSeconds":30} |
smartpot/v1/{cropId}/commands/ack |
Dispositivo → API | {"id":"…","status":"EXECUTED","message":"Bomba de agua encendida por 15 s"} |
smartpot/v1/{cropId}/status |
Dispositivo (retenido y última voluntad) | online / offline |
La telemetría también puede llegar como paquete compacto v2 (27 bytes, mismo tópico): máscara de presencia por
sensor, presión y luz de 32 bits; el formato está documentado en internal/mqtt/parser.go. Los mensajes de un mismo
cultivo se procesan en orden. La telemetría fuera de rango físico se descarta y cada cultivo guarda como máximo una lectura cada 5 segundos. Un
comando sin ACK en 2 minutos pasa a EXPIRED y avisa al dueño.
Cada lectura nueva activa al agente de automatización, que evalúa el cultivo como máximo cada 30 segundos si tiene el modo automático y cada 5 minutos si no: envía la lectura a SmartPot-AI junto con el historial reciente y los actuadores del cultivo. El servicio responde con un índice difuso de salud, el diagnóstico del sistema experto, las predicciones de los modelos y las acciones sugeridas. La API guarda el índice en el cultivo, avisa de los hallazgos críticos y, con el modo automático activo, ejecuta las acciones con un enfriamiento de 10 minutos por actuador.
Cada lectura de un cultivo real también se encola para el aprendizaje continuo (las de los virtuales son sintéticas
y no se envían): cada minuto la API envía el lote a la IA (/v1/learning/readings) con la hora local; si la IA no
responde, el lote espera al siguiente ciclo (la cola guarda hasta 20 000 lecturas). El panel general
(/api/v1/overview) mira la cuenta completa: las series comparativas se agregan en la hipertabla con time_bucket y el
análisis de flota pide a la IA el ranking, los problemas compartidos y las acciones en bloque. La evaluación también
lleva el lugar del cultivo y el clima de afuera, que el simulador consulta y la API guarda 10 minutos por lugar
en la caché.
Las notificaciones de la PWA (alertas, desconexiones, acciones del asistente y comandos) se reenvían a los canales
externos que la persona vincule y a los tipos que elija. Los canales implementan la interfaz NotificationChannel:
sumar WhatsApp, correo o Slack es una nueva implementación, sin tocar el resto. Hoy existe Telegram, centralizado en la
API (el dispositivo no habla con Telegram).
- La PWA pide un código (
POST /api/v1/channels/telegram/link) y abrehttps://t.me/<bot>?start=<código>. - El bot recibe
/start <código>, lo valida (un solo uso, 10 minutos) y vincula ese chat a la cuenta. - Desde ahí llegan los avisos con un botón para abrir el cultivo;
/estadoresume los cultivos y/desvincularcorta el envío.
Con TELEGRAM_MODE=polling la API consulta al bot con sondeo largo (sirve en local y en la demo, sin dirección
pública); en producción webhook registra PUBLIC_API_URL/api/v1/channels/telegram/webhook y valida el secreto de cada
llamada. Cada cultivo puede afinar sus avisos (crop_channels): al instante o en un resumen cada 1 a 24 horas, un
resumen diario a una hora fija y hasta 10 chats con los que se comparte. El preparador de resúmenes revisa cada minuto
los pendientes, sin cron en el servidor.
Cada persona crea desde la PWA (Perfil › Conexiones) tokens de conexión spt_… para clientes externos, como el
servidor SmartPot-MCP: GET/POST /api/v1/connections y
DELETE /api/v1/connections/{id}. El token solo se devuelve al crearlo; la base guarda su SHA-256, un prefijo para
reconocerlo y el último uso. El guard lo acepta igual que la sesión, pero con límites:
| Alcance | Puede |
|---|---|
READ |
Solo consultas (GET) |
CONTROL |
Además crear, editar, borrar y accionar actuadores |
Ninguna conexión administra conexiones, cambia la cuenta, configura canales de aviso, ve credenciales de dispositivos ni obtiene el rol de administrador. Revocar o vencer la conexión la corta al instante (401), y borrar la cuenta borra sus conexiones. Máximo 10 por cuenta.
Al crear un cultivo se elige, una sola vez, de dónde vienen sus lecturas. Uno real recibe la clave del dispositivo
para un ESP32 con el firmware, físico o simulado en Wokwi. Uno virtual no entrega
credenciales: SmartPot-DataGenerator lo simula siempre
encendido con la cuenta del cultivo, que la API descifra y le entrega por la red interna. La PWA solo habla con la API,
que comprueba el dueño y el tipo del cultivo en cada ruta. La configuración vive en la tabla virtual_devices;
pausar la simulación (DELETE) la marca active: false sin perderla y PUT la reanuda. Cada minuto la API reconcilia
con el simulador y vuelve a crear las simulaciones activas que falten. Máximo 5 por cuenta. Al arrancar, un relleno
completa el tipo y la forma de los cultivos anteriores.
La imagen es pública: ghcr.io/smartpottech/smartpot-api-go. La plataforma completa, con este servicio incluido, se
levanta con un solo comando y sin descargar el código:
curl -fsSL https://raw.githubusercontent.com/SmartPotTech/.github/main/docker/demo-beta/compose.yaml | docker compose -f - up -d --waitAbre http://localhost:5173 e ingresa con demo@smartpot.app / SmartPot2026. Se detiene con
docker compose -p smartpot-demo-beta down (con -v borra también los datos). Más detalle en la
demo beta.
- Go 1.26 (o solo Docker)
- PostgreSQL + TimescaleDB (SmartPot-DB-Postgre), Valkey (SmartPot-Cache-Valkey), Mailpit, el broker y el servicio de IA: el entorno completo está en SmartPotTech/.github
cp .env.example .env # completa secretos y conexiones
go run ./cmd/apiLa API lee .env desde la carpeta del proyecto. SMARTPOT_JWT_SECRET (al menos 32 caracteres) y SMARTPOT_AES_KEY
(32 bytes en Base64, openssl rand -base64 32) no tienen valor por defecto: sin ellos la API no arranca.
go test ./...Cubren el cifrado (incluido descifrar un texto cifrado por la versión en Java), JWT, la política de contraseñas, los rangos de medida, los tópicos y la telemetría MQTT, el aprovisionamiento en el broker, los comandos y su expiración, los switches (409), las reglas de cultivo y clima, el agente de automatización, el envío de lecturas para el aprendizaje, los canales y los avisos por cultivo, el bot de Telegram (códigos de un solo uso, webhook firmado, escape de HTML), el documento OpenAPI, la caché con respaldo local y la seguridad de dueño en los controladores.
docker build -t smartpot-api-go .
docker pull ghcr.io/smartpottech/smartpot-api-go:latestLa imagen compila el binario estático en una etapa aparte y lo copia a una imagen final scratch: sin shell, sin gestor
de paquetes y sin herramientas de red, así un contenedor comprometido no encuentra nada que aprovechar. Corre como el
usuario 1000, admite sistema de archivos de solo lectura (no escribe en disco) y el HEALTHCHECK lo resuelve el
propio
binario (smartpot-api healthcheck), sin wget ni curl. La zona horaria va incrustada en el binario y solo se suma
el
bundle de certificados CA para las conexiones TLS. Cada cambio en main pasa por el CI y publica la imagen en GHCR (y
en
Docker Hub como réplica cuando el repositorio tiene credenciales).
La documentación interactiva se sirve en /docs (interfaz de Swagger) y el esquema OpenAPI en /v3/api-docs. La
documentación técnica de la plataforma
detalla los contratos MQTT y REST, las reglas del agente, los canales y la seguridad. Los diagramas generales muestran
la plataforma completa en una sola imagen ampliable:
- IA por gRPC con respaldo REST. Con
AI_GRPC_SOCKET(socket UNIX compartido) oAI_GRPC_ADDR(TCP interno), la API llama a la IA por HTTP/2 con el mismo JSON del contrato REST y el token en los metadatos; si gRPC falla, usa REST por HTTP/1.1, que pasa por cualquier proxy. - Lecturas en vivo por SSE.
GET /api/v1/crops/{cropId}/streamenvía cada lectura validada; funciona con chunked y detrás de un nginx que hable HTTP/1.0 con la API (X-Accel-Buffering: no). El resto de los GET llevaETagy responde304si nada cambió. - Ingesta por lotes. La telemetría MQTT se guarda en lotes de hasta 100 lecturas o cada 10 ms con
COPYbinario; si un lote falla, se reintenta lectura por lectura. Los eventos (agente, aprendizaje, avisos) salen con la lectura ya guardada, y la última lectura de cada cultivo vive en la caché compartida. - Banda muerta. El agente no vuelve a consultar a la IA si ninguna variable cambió más que su umbral desde la última evaluación (0,1 °C, 0,4 % HR, 0,03 pH, 15 ppm, 0,5 % de sustrato, 0,5 hPa, 5 % de luz); al menos cada 15 minutos reevalúa.
- Detrás de un proxy. Con
TRUST_PROXY=truela IP del cliente sale deX-Forwarded-Forsolo si la conexión llega desdeTRUSTED_PROXY_CIDRS(por defecto, redes privadas); así el límite de tasa es por cliente.
Este proyecto está bajo la licencia MIT. Consulta el archivo LICENSE para más detalles.