8.7 KiB
PlantDB — Design Spec
Date: 2026-05-27 Stack: Django 5 + Bootstrap 5 + HTMX Deployment: Docker Compose on homelab
1. Overview
PlantDB is a mobile-first family plant and flower registry. It tracks both indoor and outdoor plants, stores care information per species, surfaces a pruning calendar, and integrates with the Perenual API to auto-populate species data. No user accounts — a single shared instance for the whole family.
2. Architecture
Browser (mobile-first)
│ Bootstrap 5 + HTMX (partial page updates)
▼
Nginx (reverse proxy + static/media files)
▼
Gunicorn + Django 5
▼
SQLite (single file, Docker volume)
│
└── Perenual API (external, cached on first lookup)
Key principles:
- Server-rendered Django templates throughout; HTMX handles dynamic interactions (species search live results, pruning log updates) without a separate API layer.
- Perenual API is called only once per species — results are cached in the
Speciestable. No recurring API cost after initial lookups. - All uploaded media (personal plant photos) stored in a Docker volume and served by Nginx.
3. Data Model
Species
Populated from Perenual API or entered manually. One row per plant species.
| Field | Type | Notes |
|---|---|---|
id |
PK | |
common_name |
CharField | |
scientific_name |
CharField | |
description |
TextField | |
watering |
CharField | e.g. "Weekly", "Twice a week" |
sunlight |
CharField | e.g. "Full sun", "Partial shade" |
shadow_tolerance |
CharField | e.g. "Low", "Medium", "High" |
max_height_cm |
IntegerField | nullable |
growth_rate |
CharField | e.g. "Slow", "Moderate", "Fast" |
growth_season |
CharField | e.g. "Spring–Summer" |
pruning_months |
JSONField | Default schedule, e.g. [2, 6, 8] — pre-fills the Plant field |
api_image_url |
URLField | Image from Perenual, nullable |
perenual_id |
IntegerField | Unique, nullable — prevents duplicate fetches |
Plant
One row per physical plant owned by the family.
| Field | Type | Notes |
|---|---|---|
id |
PK | |
name |
CharField | User's nickname, e.g. "Big rose on back fence" |
species |
FK → Species | Nullable — plant can exist without a species link |
location |
CharField | Free text, e.g. "Living room windowsill" |
is_indoor |
BooleanField | |
pruning_months |
JSONField | This plant's actual pruning schedule — pre-filled from species, editable per plant |
photo |
ImageField | Personal photo, stored in media volume, nullable |
notes |
TextField | Free text, nullable |
date_added |
DateField | Auto-set on creation |
PruningLog
One row per pruning event, logged by the family.
| Field | Type | Notes |
|---|---|---|
id |
PK | |
plant |
FK → Plant | |
pruned_on |
DateField | |
notes |
TextField | nullable |
4. Pages & Views
Dashboard (home /)
- Summary tiles: total plant count, number of plants due for pruning this month.
- Upcoming pruning list: overdue plants first (red), then due this month (amber).
- Buttons: "View all plants", "Add plant".
- HTMX: none needed — static server render on load.
Plant List (/plants/)
- Scrollable list of all plants, each showing name, location, indoor/outdoor badge, and thumbnail.
- Search bar at top — HTMX live search filters the list on each keystroke (targets the list partial).
- Filter chips: All / Indoor / Outdoor.
- FAB-style "+" button links to Add Plant.
Plant Detail (/plants/<id>/)
- Full-width personal photo hero (falls back to API species image if no personal photo).
- Name, scientific name (italic), indoor/outdoor badge.
- Location line.
- Care chips: 💧 watering · ☀️ sunlight · 📏 max height.
- Species reference image (small, below care info) if API image available and personal photo also exists.
- Pruning strip: recommended months + last pruned date. Amber highlight if due this month, red if overdue.
- "Log pruning" button → inline HTMX form (date + optional notes), updates pruning strip on success.
- Pruning history accordion: list of all
PruningLogentries for this plant. - Edit and Delete actions.
Add Plant (/plants/add/)
Two-step flow:
Step 1 — Species search (optional)
- Search input with HTMX live lookup against Perenual API (debounced, 300ms).
- Results show common name, scientific name, thumbnail.
- Selecting a result caches the species in the DB (if not already cached) and pre-fills the form.
- "Skip — enter manually" link bypasses to step 2 with a blank form.
Step 2 — Plant form
- Nickname field (required).
- Location field (free text).
- Indoor/Outdoor toggle.
- Care fields (watering, sunlight, etc.) — pre-filled from species if selected, all editable.
- Pruning months — pre-filled from species, shown as a 12-checkbox month picker.
- Photo section: "Take photo" (camera capture via
<input type="file" accept="image/*" capture="environment">). The species API image, if available, is always shown automatically on the plant detail as a reference — no separate action needed. - Notes textarea.
- Save button.
Edit Plant (/plants/<id>/edit/)
Same form as Add, pre-populated.
Pruning Calendar (/pruning/)
- Grouped by urgency:
- Overdue (red) — plants with a recommended pruning month in the past and no log entry since.
- This month (amber) — plants with current month in
pruning_months. - Next month (grey) — plants due next month.
- Later this year — collapsed section, expandable.
- Each entry: plant name, location, recommended months, last pruned date. Tap to open plant detail.
- Month strip at top (scrollable) to jump to any month and see what's scheduled.
Species Detail (/species/<id>/) — optional read-only
- Shows all species info as reference. Linked from plant detail. Not editable directly (edit via the plant form).
5. Perenual API Integration
- Endpoint used:
GET https://perenual.com/api/species-list?q=<name>&key=<API_KEY> - API key: stored in Django settings via environment variable
PERENUAL_API_KEY. - Cache strategy: on first successful lookup, a
Speciesrow is created withperenual_idset. Any subsequent search that returns a species already in the DB skips the API entirely. - Free tier limit: 100 requests/day. Given that lookups only happen when adding new plants (not on every page view), this is not a practical constraint for a family app.
- Fallback: if the API is unreachable or returns an error, the add-plant form stays open with an empty species form for manual entry. No hard dependency on the API.
6. Pruning Logic
A plant is considered overdue for pruning if:
- It has at least one
pruning_monththat fell within the past 12 months, AND - There is no
PruningLogentry withpruned_onfalling within that month's window.
A plant is due this month if the current month is in Plant.pruning_months and not yet logged this month.
Using a 12-month rolling window (rather than calendar-year) correctly handles plants due in December when checked in January.
The pruning calendar view computes this in a Django queryset annotated with the current date — no background tasks or scheduled jobs needed.
7. Deployment
Docker Compose
services:
web: Django + Gunicorn
nginx: Reverse proxy + static/media serving
volumes:
db: SQLite file
media: Uploaded plant photos
- Accessible on the local network via homelab IP or a local hostname (e.g.
plantdb.home). - No SSL required for LAN use.
docker compose up -dstarts everything;docker compose downstops it.- Database backup: copy the SQLite volume file. Simple enough to add to an existing homelab backup routine.
Environment Variables
| Variable | Purpose |
|---|---|
PERENUAL_API_KEY |
Perenual API key |
SECRET_KEY |
Django secret key |
DEBUG |
False in production |
ALLOWED_HOSTS |
Homelab hostname/IP |
8. Static & Media Files
- Static files (CSS, JS, Bootstrap): collected via
manage.py collectstatic, served by Nginx. - Media files (plant photos): stored in
/media/inside the container, mounted as a Docker volume, served by Nginx under/media/.
9. Dependencies
| Package | Purpose |
|---|---|
django |
Web framework |
gunicorn |
WSGI server |
django-htmx |
HTMX integration helpers |
pillow |
Image handling for ImageField |
requests |
Perenual API calls |
No frontend build step — Bootstrap 5 and HTMX loaded via CDN or bundled as static files.
10. Out of Scope
- User authentication / accounts
- Watering or feeding reminders
- Push notifications
- Multi-language support
- Public internet access / SSL (LAN only)
- Offline / PWA mode