flowl — short for flower owl — /flaʊl/ like "fowl" but with an l
A small Rust service that exposes plant care data (watering schedules, care needs, etc.) for integration with Home Assistant and other automation platforms.
- Watering at a glance — see which plants need water and act with one tap
- Care journal — log and edit watering, fertilizing, repotting, pruning, and custom events with optional photos
- AI plant identification — snap a photo, get the species and a full care profile
- AI care assistant — ask plant-specific questions in a chat, save the advice to your journal
- Home Assistant integration — each plant appears as an MQTT sensor with watering status
- Backup & restore — export and import all data and photos as a ZIP
- Works everywhere — responsive on phone, tablet, and desktop; installable as a PWA with offline support
- Optional OIDC authentication — protect the UI, API, and uploads with a generic OpenID Connect provider
- Light & dark theme — with English, German, and Spanish translations
- Single binary — self-contained Rust binary with embedded UI, just run it or use Docker
docker run -p 4100:4100 -v flowl-data:/data \
-e FLOWL_MQTT_DISABLED=true \
ghcr.io/simplyroba/flowl:latestOpen http://localhost:4100. Data is persisted in the flowl-data volume.
A docker-compose.yml is included in the repository. Uncomment the environment variables to enable all features.
docker compose up -d| Variable | Default | Description |
|---|---|---|
FLOWL_PORT |
4100 |
HTTP server listen port. |
FLOWL_DB_PATH |
/data/flowl.db |
Filesystem path to the SQLite database. |
FLOWL_LOG_LEVEL |
info |
tracing level filter for logs. |
FLOWL_MQTT_HOST |
localhost |
MQTT broker hostname. |
FLOWL_MQTT_PORT |
1883 |
MQTT broker port. |
FLOWL_MQTT_TOPIC_PREFIX |
flowl |
Topic prefix used for auto-discovery and plant topics. |
FLOWL_MQTT_DISABLED |
false |
Skip MQTT client, state checker, and publishes when set to true. |
FLOWL_AI_API_KEY |
— | API key for the OpenAI-compatible AI provider. AI features are disabled when unset. |
FLOWL_AI_BASE_URL |
https://api.openai.com/v1 |
Base URL for the AI API. |
FLOWL_AI_MODEL |
gpt-4.1-mini |
Model name used for all AI tasks. |
FLOWL_AI_RATE_LIMIT |
10 |
Max AI requests per minute (0 to disable). |
FLOWL_AUTH_ENABLED |
false |
Enable built-in OIDC authentication. Only true or false is accepted. |
FLOWL_EXTERNAL_URL |
— | Required when auth is enabled. Public HTTPS origin of Flowl, with no path, query, or fragment. |
FLOWL_OIDC_ISSUER |
— | Required when auth is enabled. Exact HTTPS issuer identifier advertised by the provider. |
FLOWL_OIDC_CLIENT_ID |
— | Required confidential-client identifier. |
FLOWL_OIDC_CLIENT_SECRET |
— | Required confidential-client secret. |
FLOWL_OIDC_PROVIDER_NAME |
OpenID Connect |
Provider name shown on the login page. |
Authentication is disabled by default. Configure the variables above and register <FLOWL_EXTERNAL_URL>/auth/callback on a confidential OpenID Connect client using Authorization Code flow.
The public URL and provider endpoints must use HTTPS; Flowl itself may use HTTP behind a TLS-terminating proxy. The issuer must exactly match the provider value, including any trailing slash.
Sessions are process-local and expire after 12 hours. Restarts sign everyone out, and multi-instance deployments require sticky routing. When enabled, Flowl protects the API, uploads, and application pages; /health and login resources remain public.
The model must support vision (image input), structured output (response_format: json_schema), and streaming (SSE).
| Model | Vision | Structured output | Streaming | Notes |
|---|---|---|---|---|
gpt-5-mini |
Yes | Yes | Yes | GPT-5 family, most capable but significantly more expensive |
gpt-4.1-mini |
Yes | Yes | Yes | Successor to gpt-4o-mini with better performance |
gpt-4.1-nano |
Yes | Yes | Yes | Cheapest OpenAI option, outperforms gpt-4o-mini |
gpt-4o-mini |
Yes | Yes | Yes | Previous generation, still capable |
| Model | Vision | Structured output | Streaming | Notes |
|---|---|---|---|---|
mistral-small-latest |
Yes | Yes | Yes | Best value — 24B, strong vision and structured output |
ministral-14b-latest |
Yes | Yes | Yes | 14B edge model, successor to pixtral-12b, Apache 2.0 |
To use Mistral, set FLOWL_AI_BASE_URL=https://api.mistral.ai/v1 and FLOWL_AI_API_KEY to your Mistral API key.
Any other provider exposing an OpenAI-compatible /v1/chat/completions endpoint (LM Studio, vLLM, Ollama with OpenAI shim, etc.) should also work as long as the model supports the capabilities above.
flowl works as an installable PWA and remains usable when your device can't reach the server.
What works offline:
- Browse the dashboard, plant details, and care journal using cached data from your last visit.
- View cached plant photos and thumbnails.
- Switch theme and language (stored locally).
What's disabled offline:
- Watering and adding, editing, or deleting log entries.
- Editing or deleting plants.
- Creating new plants or editing existing ones.
- AI chat and plant identification.
- Data export and import.
An orange dot on the Settings icon indicates the app is offline. Pages that can't load any cached data show an offline message. When the server becomes reachable again, all controls re-enable automatically.
With OIDC enabled, session expiry does not erase offline data. Sign out purges protected caches while retaining login resources and theme/language preferences.
Flowl includes optional built-in OIDC authentication for the UI, API, and uploads. It does not add users, roles, per-user data, or provider logout; every accepted identity accesses the same data. If built-in authentication is disabled, use a trusted network or an authenticating reverse proxy.
With MQTT enabled, each plant appears as a sensor entity (sensor.flowl_<name>) via auto-discovery. The state is ok, due, or overdue. Attributes include last_watered, next_due, and watering_interval_days.
automation:
- alias: "Thirsty plants notification"
trigger:
- platform: time
at: "08:00:00"
condition:
- condition: template
value_template: >
{{ states.sensor
| selectattr('entity_id', 'match', 'sensor.flowl_')
| selectattr('state', 'in', ['due', 'overdue'])
| list | count > 0 }}
action:
- service: notify.mobile_app_your_phone
data:
title: "Plants need water"
message: >
{% set thirsty = states.sensor
| selectattr('entity_id', 'match', 'sensor.flowl_')
| selectattr('state', 'in', ['due', 'overdue'])
| list %}
{{ thirsty | count }} {{ 'plant needs' if thirsty | count == 1 else 'plants need' }} water:
{{ thirsty | map(attribute='name') | map('replace', 'flowl ', '') | join(', ') }}Copyright (C) 2026 simplyRoba.
This project is developed spec-driven with AI assistance, reviewed by a critical human.




