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 OIDC login. |
FLOWL_EXTERNAL_URL |
— | Required public HTTPS URL of Flowl, without a subpath. |
FLOWL_OIDC_ISSUER |
— | Required OIDC issuer URL. |
FLOWL_OIDC_CLIENT_ID |
— | Required confidential-client identifier. |
FLOWL_OIDC_CLIENT_SECRET |
— | Required confidential-client secret. |
FLOWL_OIDC_PROVIDER_NAME |
OpenID Connect |
Name shown on the login button. |
FLOWL_OIDC_AUTO_REDIRECT |
false |
Skip the login-button click. |
Register <FLOWL_EXTERNAL_URL>/auth/callback with a confidential Authorization Code client supporting client_secret_basic or client_secret_post.
Flowl serves HTTP behind a TLS-terminating reverse proxy. Public and provider URLs require HTTPS; the provider needs a publicly trusted certificate. Use the issuer URL exactly as advertised, including any trailing slash. The provider must be reachable at startup.
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 cached data. Use Sign out to clear cached plant data and photos on shared devices.
Enable OIDC or use an authenticating reverse proxy before exposing Flowl outside a trusted network.
- All signed-in users share the same data. Restrict access to the OIDC client at your provider.
- Sessions end 12 hours after login or on restart. Use one instance or sticky routing; instances do not share sessions.
- Signing out ends only the Flowl session, not the provider session.
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.




