A garden planning and plant lifecycle tracking system with three components: a Quarkus backend, an Android app, and a React admin UI.
Quarkus + Kotlin REST API with PostgreSQL.
- Plant lifecycle: Sow seeds (in beds or portable trays), pot up, plant out, harvest, recover, discard — each as a tracked event
- Species management: System-wide and user-created species with Swedish/English names, growing conditions, photos, and seed provider links
- Garden structure: Gardens with geo-located beds and boundary polygons
- Seed inventory: Track seed batches with collection/expiration dates, auto-decrement on sowing
- Scheduled tasks: Recurring garden activities with deadlines and progress tracking
- AI integration: Gemini-powered extraction of species info from seed packet photos
- Storage: Google Cloud Storage for images
- Auth: JWT-based authentication with Google OAuth for the app and email/password for admin
Kotlin Android app with Jetpack Compose.
- My World: Dashboard with gardens, tray plant summary, and harvest stats
- Plants: Species-grouped view with current locations, batch actions (pot up, plant out, harvest, recover, discard) via modal
- Tasks: Scheduled activities with species-specific workflows
- Sowing: Select species, choose bed or portable tray, auto-creates individual plants
- Gardens: Map-based garden/bed creation with boundary drawing, inline editing
- Seed inventory: Track and manage seed batches with FAB to add
- Swedish-first: All UI strings localized, Swedish names shown as primary
React + TypeScript + Tailwind CSS (Notion-inspired design).
- Species CRUD: Create, edit, delete species with image uploads, AI extraction, provider linking
- Users/Gardens: View and manage registered users and their gardens
- Providers: Manage seed providers (Impecta, Florea, Wexthuset, etc.)
- Import/Export: JSON export/import of species data including provider info
- Bundled with backend: Built into the Quarkus JAR and served as static files at
/
- JDK 21
- Node.js 22+
- Android Studio (for the Android app)
- Docker (for local PostgreSQL and deployment builds)
Alternatively, use the Dev Container (see below) — it bundles JDK 21, Node 22, and Claude Code CLI, and wires PostgreSQL automatically.
The repo ships a Dev Container (mcr.microsoft.com/devcontainers/java:21-bookworm + Node 22 + Claude Code CLI + Android SDK platform-35) that runs the backend, admin UI, and Android build alongside a PostgreSQL container.
VS Code: open the folder and choose "Reopen in Container". The workspace mounts at /workspaces/verdant and ports 8081 (backend) and 5174 (admin) are auto-forwarded.
Plain Docker:
Start the containers:
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml up -dOpen a shell inside the dev container (workspace is already at /workspaces/verdant):
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml exec dev bashThen, inside the container, start the backend:
cd backend && ./gradlew quarkusDevRebuilding after a Dockerfile change (e.g. picking up a new bundled Claude Code CLI):
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml rm -sf dev
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml build --no-cache dev
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml up -d dev
docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml exec dev bashWith the official devcontainer CLI (npm i -g @devcontainers/cli) the same flow is two commands:
devcontainer up --workspace-folder . --remove-existing-container
devcontainer exec --workspace-folder . bashThe host's ~/.claude and ~/.config/gcloud are bind-mounted into the container so Claude Code auth/memory/plugins and gcloud/cloud-sql-proxy credentials follow you in (no re-auth required). Gradle and npm caches live in named volumes (gradle-cache, node-modules-cache) so they survive container rebuilds. The backend reaches PostgreSQL at postgres:5432 inside the network; the host can reach it on localhost:5433.
The Android app compiles, tests, and lints inside the container — cd android && ./gradlew :app:compileDebugKotlin (or :app:assembleDebug, :app:testDebugUnitTest, :app:lintDebug) all work against the bundled SDK at /opt/android-sdk. The container resolves the SDK via ANDROID_HOME rather than writing local.properties, so host-side Android Studio builds keep working without manual cleanup. android/settings.gradle.kts auto-deletes a stale local.properties whose sdk.dir points at a directory that doesn't exist in the current environment — so opening the project in Android Studio on the host (which regenerates the file with the host's SDK path), and then jumping into the container, just works. For CLI-only host builds, set ANDROID_HOME=$HOME/Library/Android/sdk in your shell profile.
Interactive debugging on a device or emulator still needs the host: Docker Desktop on macOS doesn't expose KVM or USB. To adb into a host-side emulator/device from inside the container, run adb tcpip 5555 on the host once, then adb connect host.docker.internal:5555 inside the container. For running the app interactively, set the Android app's .env.yaml API host to your laptop IP and the forwarded :8081 port.
cd backend
cp .env.yaml.template .env.yaml # edit with your keys
./gradlew quarkusDev # starts on port 8081, auto-provisions PostgreSQLcd admin
npm install
npm run dev # starts on port 5174, proxies /api to backendcd android
cp .env.yaml.template .env.yaml # set your laptop IP and API keysOpen in Android Studio and run on device/emulator.
To populate the database with 3 seasons of realistic flower production data, first log in with Google to create your account, then:
# Local development
curl -X POST http://localhost:8081/api/dev/seed
# Production
curl -X POST https://verdantplanner.com/api/dev/seed
# For a different user
curl -X POST https://verdantplanner.com/api/dev/seed?email=someone@example.comDefaults to erik@l2c.se. Creates 16 cut flower species, 5 customers, 6 beds, ~45 plants across 2024-2026 with full harvest data, succession schedules, production targets, variety trials, bouquet recipes, and pest/disease logs.
The schema is managed by Flyway with a single migration (V1__schema.sql). Flyway runs automatically on startup (quarkus.flyway.migrate-at-start=true).
cd backend
./gradlew dbBackup # backup to db-backups/
./gradlew dbRestore # restore latest backup-
Start the Cloud SQL Auth Proxy:
cloud-sql-proxy verdant-planner-staging:europe-north1:verdant-staging --port 5433
-
Fetch the password from Secret Manager:
gcloud secrets versions access latest --secret=verdant-db-password --project=verdant-planner-staging
-
Connect with psql:
PGPASSWORD=$(gcloud secrets versions access latest --secret=verdant-db-password --project=verdant-planner-staging) \ psql -h localhost -p 5433 -U verdant -d verdant
Install the proxy with brew install cloud-sql-proxy if needed.
Drop and recreate the public schema to force Flyway to re-run all migrations on next startup. This destroys all data.
# Local
PGPASSWORD=verdant psql -h localhost -p 5433 -U verdant -d verdant \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# Production (with proxy running)
PGPASSWORD=$(gcloud secrets versions access latest --secret=verdant-db-password --project=verdant-planner-staging) \
psql -h localhost -p 5433 -U verdant -d verdant \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"Deployed to Google Cloud Run with Cloud SQL (PostgreSQL).
# One-time setup
./deploy/setup-gcp.sh <PROJECT_ID> <REGION>
# Store secrets
gcloud secrets create verdant-gemini-key --data-file=<(echo -n 'KEY')
gcloud secrets create verdant-admin-password --data-file=<(echo -n 'PASSWORD')
# Configure Docker auth
gcloud auth configure-docker <REGION>-docker.pkg.dev
# Build and deploy (backend + admin UI in one container)
./deploy/deploy.sh <PROJECT_ID> <REGION>Each module has its own .env.yaml (not checked in):
backend/.env.yaml— Gemini API key, GCS credentials, admin password, database connectionandroid/.env.yaml— Backend API URL, Google OAuth client ID, Maps API key
Deployed at verdantplanner.com