Plant identification app with smart watering reminders
Mobile app that identifies plants from photos and schedules offline watering and fertilising reminders adjusted by species, season and local weather.
react-nativeexpotypescriptfirebasecloud-functionsrevenuecat
Built 1 time, downloaded 0 times.
Install from your terminal
Run this in your project folder. It signs you in, shows what changes and asks before it overwrites a file.
npx claude-lab add plant-identification-app-with-smart-watering-reminders-c7df0bCLAUDE.md# Plant care app
A mobile app that identifies plants from photos and sends reliable watering and fertilising reminders. Each reminder is based on the species, the season and local weather in the user's city. The app is free and shows ads. The paid tier removes ads and adds a plant diary with side-by-side growth photos.
Stack: React Native with Expo (managed workflow, EAS Build, dev client), TypeScript in strict mode, and Firebase (Auth, Firestore, Storage, Cloud Functions, FCM).
Specs and plan: see SPEC.md (product spec) and PLAN.md (phased build plan).
## Commands
Fill in and confirm these after Phase 1 scaffolding.
- `npx expo start`: start the dev server
- `npx tsc --noEmit`: type check
- `npx eslint .`: lint (CI uses `--max-warnings=0`)
- `npx jest`: unit tests
- `eas build --profile development`: dev client for real devices
- `cd functions && npm test`: Cloud Functions tests
- `firebase emulators:start`: local Auth, Firestore, Storage and Functions
## Folder map
- `app/`: Expo Router screens
- `src/features/identify/`: photo capture and plant ID flow
- `src/features/reminders/`: schedules, local notifications, snooze and done
- `src/features/diary/`: premium growth photo diary
- `src/features/paywall/`: subscription and ad gating
- `src/lib/firebase/`: Firebase init from env variables
- `functions/`: scheduling, weather refresh, notification sender, plant ID proxy
## Notification rules
Reminders are the core feature. Treat them as the highest-risk area.
- Use local scheduled notifications (expo-notifications) for the per-plant watering schedule so they work offline.
- Use FCM only for server-driven weather adjustments.
- Never rely on background timers.
- Keep a rolling window of scheduled notifications. Re-fill on app open.
Platform gotchas:
- Android 13 and later need the POST_NOTIFICATIONS runtime permission and a notification channel.
- Android exact alarms and battery optimisation can delay reminders.
- iOS allows 64 pending local notifications. Schedule a window, not everything.
- Push does not work on simulators. Test on a real device with a dev build.
- Expo Go cannot test notifications, ads or purchases. Use a development build.
## Data rules
- Store each user's timezone and city.
- Compute reminder times in the user's local timezone and handle DST.
- Location is city-level only. Ask for permission with a clear reason.
- Photos go under the user's uid path. Storage and Firestore rules allow only the owner.
## Secrets and services
- The plant ID provider is called only from a Cloud Function. The API key stays server-side.
- Keys come from env variables or Firebase secrets. Never commit them.
- Use placeholders such as `YOUR_API_KEY`, `YOUR_WEATHER_API_KEY` and `YOUR_REVENUECAT_KEY`.
- Ads use react-native-google-mobile-ads. Use test ad unit IDs in development.
- Subscriptions use RevenueCat. The entitlement `premium` gates ad removal and the diary.
## Conventions
- Functional components and hooks only. No `any`.
- One feature folder per feature. Keep PRs small.
- Test Firestore security rules with the emulator.
- Every screen needs loading and error states.
- Give camera and reminder controls accessibility labels.
## Workflow
1. Explore the code first.
2. Plan the change.
3. Code it.
4. Run the verify skill before saying a task is done.
Use `/clear` between unrelated tasks. Use `/compact` at natural breaks.
Review rules are in REVIEW.md, imported below so `/code-review` reads them.
@REVIEW.md
.claude/agents/test-runner.md---
name: test-runner
description: Runs the project's verification commands and returns only failing test names, file lines and short error messages. Use when tests or type checks need running without filling the main context with logs.
model: haiku
tools: Bash, Read, Grep
maxTurns: 8
---
You run the project's checks and report only what failed.
Run the commands from the verify skill (`.claude/skills/verify/SKILL.md`), in this order:
1. `npx tsc --noEmit`
2. `npx eslint . --max-warnings=0`
3. `npx jest --silent`
4. `cd functions && npm test`, only if the caller says functions changed
5. `firebase emulators:exec --only firestore "npm test"`, only if the caller says rules changed
If the caller names a specific test file or pattern, pass it to jest (`npx jest <pattern> --silent`) and run nothing else.
Report format:
- One line per command: `PASS` or `FAIL`.
- For each failure: the test name or rule or lint rule, the `file:line`, and a message of one line, at most 200 characters.
- Stop after 10 failures per command and say how many were left out.
- If a command could not run (missing binary, missing dependencies), report `SKIPPED` and the reason in one line.
Rules:
- Do not paste full logs, stack traces or passing test output.
- Do not edit, create or delete any file, and do not run `npm install`, `git` write commands or `eas`/`firebase deploy`.
- Do not print the contents of `.env*` files or any secret, even if an error message contains one. Replace it with `[redacted]`.
- Never report success for a step that was skipped or did not run.
.claude/settings.json{
"permissions": {
"allow": [
"Bash(npx tsc --noEmit)",
"Bash(npx eslint *)",
"Bash(npx jest *)",
"Bash(npx expo start*)",
"Bash(git status)",
"Bash(git diff*)",
"Bash(git log*)"
],
"ask": [
"Bash(eas build*)",
"Bash(eas submit*)",
"Bash(firebase deploy*)",
"Bash(git push*)",
"Bash(npm install*)"
],
"deny": [
"Read(./.env*)",
"Read(./secrets/**)",
"Edit(**/GoogleService-Info.plist)",
"Write(**/GoogleService-Info.plist)",
"Bash(rm -rf*)"
],
"blockWritesOutsideWorkingDirectories": true
},
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -n \"$f\" ] && npx --no-install prettier --write \"$f\" >/dev/null 2>&1; exit 0"
}
]
}
]
}
}
.claude/skills/notification-scheduler/SKILL.md---
name: notification-scheduler
description: Use when creating or changing watering and fertilising reminder scheduling, notification permissions, the rolling notification window, or weather-driven schedule updates.
paths:
- "src/features/reminders/**"
- "functions/src/scheduling/**"
---
# Notification scheduler
Reminders are the core feature. They must fire on time, offline, from the device, without relying on background timers. Server pushes only adjust schedules; they do not replace local reminders.
## Scheduling algorithm
Compute each reminder in this order:
1. Base interval: water and fertiliser intervals in days from the plant's care profile (`careProfiles`).
2. Season multiplier: from the user's hemisphere and the current month. Read it from the profile, not hard-coded at call sites.
3. Weather adjustment (Phase 4): for outdoor plants, skip or delay watering after rain; shorten the water interval during heat waves. Apply only when the value is present and fresh. Otherwise use the base schedule.
4. Next due date: anchor to the user's stored timezone (`users/{uid}.timezone`), not the device's current zone, at the user's preferred reminder time. Store the result as an absolute UTC instant, and derive the local time for display.
## Rolling window
- Keep the next 30 to 50 local notifications scheduled across all plants, sorted by due time.
- iOS allows at most 64 pending local notifications per app. Stay well under that cap.
- Re-fill the window on app open and when an FCM data message arrives.
- Cancel and reschedule a plant's notifications whenever its profile, snooze, done state or timezone changes.
- Never leave orphaned notifications for deleted plants.
## Platform rules
Android:
- Create the reminders notification channel before scheduling anything. Channel settings apply at creation, so define them once and version the channel ID if they change.
- Request `POST_NOTIFICATIONS` at runtime on Android 13 and later, at a moment the user understands, not on first launch.
- Exact alarms and battery optimisation can delay delivery. Do not promise exact timing in UI copy.
- Request exact alarm access only if the design needs it, and document why.
iOS:
- Ask for notification permission in context: after the user adds their first plant, with a clear reason.
- Respect the 64 pending notification cap (see above).
- Push does not work on simulators. Test on a real device with a development build.
## Permission denied
If permission is denied:
- Keep plants and schedules visible in the app.
- Show a banner that reminders are off, with a link to system settings.
- Do not repeat the system prompt automatically.
## Reinstall, reboot and timezone change
- After reboot or reinstall, the schedule is rebuilt from Firestore on the next app open. Nothing depends on notifications surviving.
- A timezone change recalculates every due time in the new zone and refills the window.
## Test cases that must exist
Every change in this area needs tests for:
- DST change: local reminder time stays the same across spring-forward and fall-back days.
- Timezone change: reminders move with the user's new zone.
- Permission denied: no scheduling attempts, and the banner state is shown.
- Reinstall: schedule is rebuilt from Firestore on first open.
- Snooze: the due item moves and the old notification is cancelled.
- Mark done: the next due date is recalculated and rescheduled.
- 64 limit: the window never exceeds the cap.
Worked examples and failure modes are in `edge-cases.md` in this folder.
.claude/skills/release-checklist/SKILL.md---
name: release-checklist
description: Store release preparation checklist for production builds, run only when the user invokes it before any EAS build or store submission.
disable-model-invocation: true
---
# Release checklist
Run this only when the user invokes it. Work through each item, mark it done or open, and report the full list.
Never write real keys, tokens or certificates into the repo. Use placeholders such as YOUR_ADMOB_PROD_ID and YOUR_REVENUECAT_PROD_KEY.
## Before building
- [ ] Production AdMob ad unit IDs are set through EAS environment variables for the production profile, not in code or `app.json`.
- [ ] The RevenueCat production key is set through EAS environment variables. The sandbox key is not used in production.
- [ ] The Firebase production project is selected, and the Firestore rules tests pass.
- [ ] The verify skill passes in full.
- [ ] A real-device notification pass is complete on one iOS device and one Android 13+ device, using `docs/notification-checklist.md`.
- [ ] Permission usage strings in `app.json` are present and specific: camera, photo library, and approximate location.
- [ ] The privacy policy URL is live and linked from the app settings and the store listing.
## Store forms
- [ ] App Store privacy labels match what the app collects: account, photos, city-level location, purchase data, and ad identifiers if used.
- [ ] The Google Play data safety form matches the same data, including what is shared with ad and purchase SDKs.
- [ ] Store listing screenshots, description and age rating are complete.
## Build and submit
Stop and ask the user to confirm before running any of these:
- `eas build --profile production`
- `eas submit` for each platform
Do not run `eas build` or `eas submit` without explicit confirmation in the current conversation. After confirmation, run the build and report the profile and build ID.
## Report
List every item as done, open, or not applicable. Give a one-line reason for each open item. Mark an item done only if you checked it in this session.
.claude/skills/verify/SKILL.md---
name: verify
description: Run the project checks after any code change and before finishing a task, reporting pass or fail for each step.
when_to_use: Use after any code change, before committing, before opening a pull request, and before saying a phase goal is met.
---
# Verify
Run these steps in order. Stop at the first failure, fix it, and rerun from that step. Do not claim success until every applicable step has passed.
## Steps
1. Types: `npx tsc --noEmit`
2. Lint: `npx eslint . --max-warnings=0`
3. Unit tests: `npx jest --silent`
4. Functions, only if anything under `functions/` changed: `cd functions && npm test`
5. Firestore rules, only if `firestore.rules` or its tests changed: `firebase emulators:exec --only firestore "npm test"`
6. Bundle smoke run: `npx expo export --platform android` and `npx expo export --platform ios`. Run both, since each platform can fail on its own.
## Notification changes
If anything under `src/features/reminders/`, `functions/src/scheduling/` or the notification setup in `app.json` changed, tell the user to run the real-device checklist in `docs/notification-checklist.md`. Simulators cannot prove delivery, so a passing run here does not mean reminders will arrive.
## Report format
For each command, list the command and PASS or FAIL. For failures, show only the first failing lines with file and line. Do not paste full logs.
Example:
- `npx tsc --noEmit`: PASS
- `npx eslint . --max-warnings=0`: FAIL
- `src/features/reminders/schedule.ts:42` error: unexpected any
## Skipped steps
Never claim success when a step was skipped. Name each skipped step and the reason, for example: "Functions tests skipped: no changes under functions/."
A phase is not done until this skill passes in full.
INSTALL.md# Install and first run
This guide assumes you have used Git, the terminal and a mobile app build before.
## Prerequisites
- Node LTS and Git
- Claude Code, updated to the latest version
- An Expo account
- A Firebase project
- Apple Developer and Google Play accounts (needed later, for release)
- A real iOS device and a real Android 13+ device. Simulators cannot prove notification delivery.
## Set up the folder
1. Create the project folder and run `git init`.
2. Copy all generated files into it, keeping their paths. The `.claude/` folder must sit at the project root.
3. Install the CLI tools:
```
npm install -g eas-cli firebase-tools
eas login
firebase login
```
## Install plugins
Run `claude` in the project folder, then run:
```
/plugin install feature-dev@claude-plugins-official
/plugin install code-review@claude-plugins-official
/plugin install commit-commands@claude-plugins-official
/plugin install security-guidance@claude-plugins-official
```
## Fill in SPEC.md
Answer the open questions in `SPEC.md`, especially the plant identification provider and the weather API. Claude reads this file in Phase 1 and later phases depend on it.
## Keys and secrets
- Keep local keys in `.env.local`. This file must be listed in `.gitignore`. Use placeholders until you have real values, for example `YOUR_API_KEY`, `YOUR_WEATHER_API_KEY` and `YOUR_REVENUECAT_KEY`.
- Store server-side keys as Firebase secrets, for example `firebase functions:secrets:set YOUR_SECRET_NAME`.
- Never commit real keys. The `.claude/settings.json` deny rules block reading `.env*` files, but you are still responsible for keeping keys out of git.
## Firebase emulators
Start the emulators from the project root before running Firestore rules tests or the Phase 2 emulator tests:
```
firebase emulators:start
```
## Build a development client
Expo Go cannot test notifications, ads or purchases, so use a development build on your real devices:
```
eas build --profile development
```
Install the build on each device. You will need to run it again when native dependencies change.
## Run Phase 1
1. In `claude`, paste the Phase 1 kickoff prompt from the plan. It starts with `/goal`.
2. A `/goal` run stops when a small model judges the condition met. That model only sees what Claude has shown in the conversation, so keep the verify output visible.
3. Phase 1 stops after at most 25 turns and lists anything unfinished. Review that list before continuing.
4. Run `/clear` before starting Phase 2.
Repeat for each phase in `PLAN.md`. Do the manual device checks listed in each phase.
## Token habits
- Run `/clear` between unrelated tasks.
- Run `/compact` at natural breaks in a long task.
- Run `/usage` after each phase to see where tokens went.
- Change reasoning depth with `/effort` instead of switching models. Changing the model breaks the prompt cache, and changing effort does not on most current models.
## Troubleshooting
**Android notifications do not arrive**
- Check that the app has notification permission (Android 13+ asks at runtime).
- Check that the reminder channel exists in the app's notification settings.
- Set battery usage for the app to unrestricted. Battery optimisation can delay reminders.
- Check that exact alarm permission is granted if the app uses exact alarms.
**iOS push does not arrive**
- Upload an APNs auth key to Firebase: Project settings, Cloud Messaging, Apple app configuration.
- Push does not work on the simulator. Test on a real device with a development build.
- Check that you accepted the notification permission prompt.
PLAN.md# Build plan
How to run each phase:
- Run `/clear` first, then paste the phase's `/goal` text as the first message.
- If a phase stalls, change effort with `/effort`. Do not switch models mid-phase.
- Run `/usage` at the end of each phase.
- Manual checks need a real device and a development build. Simulators cannot prove notification delivery.
## Phase 1: scaffold
**Scope:** Expo with TypeScript strict and Expo Router. Firebase wiring with emulators. Lint, format and test setup. `eas.json` profiles. Commands filled in CLAUDE.md.
**Files likely touched:** `package.json`, `tsconfig.json`, `app.json`, `eas.json`, `app/_layout.tsx`, `app/index.tsx`, `src/features/*/`, `src/lib/firebase/`, `functions/`, `firebase.json`, ESLint, Prettier and Jest config, `CLAUDE.md`.
**Manual checks (real device):**
- Run `eas build --profile development` for iOS and Android.
- Install both dev builds and confirm the app opens to the home screen.
**/goal:**
```text
/goal Phase 1 scaffold is done when `npx tsc --noEmit`, `npx eslint . --max-warnings=0`, `npx jest` and `cd functions && npm test` all exit 0, and `npx expo export --platform android` succeeds. Each feature folder (identify, reminders, diary, paywall) has one passing smoke test. Firebase reads config from env variables with placeholders only. firebase.json configures Auth, Firestore, Storage and Functions emulators. The Commands section of CLAUDE.md lists the exact working commands. Constraints: do not install ad or purchase SDKs, do not run eas build or firebase deploy, do not write secrets, do not edit SPEC.md. Run the verify skill and paste its pass/fail output. Do not claim the dev build boots; the user checks that. Stop after 25 turns and list anything unfinished.
```
## Phase 2: auth and plant ID
**Scope:** Firebase Auth. Camera and photo picker with accessibility labels. Upload to Storage under `users/{uid}/plants/`. Cloud Function that calls the identification provider. Confirm or correct screen. Plant saved to Firestore.
**Files likely touched:** `src/features/identify/`, `src/lib/firebase/auth.ts`, `functions/src/identify/`, `firestore.rules`, `storage.rules`, matching tests.
**Manual checks (real device):**
- Sign in with email link on both devices.
- Photograph a real plant and check the suggestion is shown.
- Correct a wrong suggestion and check the saved species.
**/goal:**
```text
/goal Phase 2 auth and plant ID is done when: Firebase Auth sign-in works in the emulator; the identify Cloud Function has mocked-provider tests that pass; the Firestore and Storage rules emulator test shows only the owner can read or write a plant; `grep -r` finds no provider key or provider URL in src/ or app/; the confirm and correct screen is covered by a passing component test with loading and error states. Use the placeholder YOUR_API_KEY only. Run `npx tsc --noEmit`, `npx eslint . --max-warnings=0`, `npx jest`, `cd functions && npm test`, and the verify skill; paste the output. Constraints: the client never calls the provider; do not edit SPEC.md; do not run firebase deploy. Stop after 30 turns and list anything unfinished.
```
## Phase 3: care profiles and reminders (core)
**Scope:** Care profile data and seeding. Scheduler with season logic as pure functions. Rolling window of local notifications (30 to 50). Permission flow. Android notification channel. Mark done and snooze. Re-fill on app open. Start `docs/notification-checklist.md`.
**Files likely touched:** `src/features/reminders/`, `src/lib/notifications/`, `functions/src/scheduling/`, `firestore.rules`, `docs/notification-checklist.md`, `__tests__/` for scheduler edge cases.
**Manual checks (real device):**
- iOS: grant permission, set a reminder two minutes ahead, lock the phone, confirm delivery.
- Android 13 or newer: check the permission prompt and the notification channel in system settings.
- Deny permission and confirm the in-app due list and banner appear.
- Change the device timezone, reopen the app, and confirm the next reminder moves.
- Android: check battery optimisation settings and note any delay.
**/goal:**
```text
/goal Phase 3 reminders is done when unit tests pass for: DST spring-forward and fall-back in the user's timezone; timezone change recomputes the schedule; the rolling window never exceeds 50 pending notifications and never more than 64; permission denied falls back to the in-app list; snooze cancels the old notification and respects snoozedUntil; mark done schedules the next reminder from today. docs/notification-checklist.md exists with a section for each platform. Run `npx tsc --noEmit`, `npx eslint . --max-warnings=0`, `npx jest`, `cd functions && npm test` and the verify skill; paste the output. Note that real-device delivery is for the user to confirm. Constraints: do not edit SPEC.md; do not run eas build. Stop after 35 turns and list anything unfinished.
```
## Phase 4: weather adjustment
**Scope:** Daily scheduled Cloud Function that fetches city weather once per city. Apply the rain delay and heat-wave rules from SPEC.md. FCM data message that triggers a local reschedule. Fallback when the weather API fails.
**Files likely touched:** `functions/src/weather/`, `functions/src/scheduling/`, `src/features/reminders/` (FCM handler), `src/lib/firebase/messaging.ts`, tests.
**Manual checks (real device):**
- Send a test FCM data message from the Firebase console to the dev build and confirm the local schedule updates.
- Set `YOUR_WEATHER_API_KEY` as a Firebase secret, not in code, and confirm the function runs.
**/goal:**
```text
/goal Phase 4 weather is done when function tests pass against mocked weather responses for: rain of 5 mm or more delays watering by one day once per cycle; a heat wave of 32 C or more for 3 days shortens the interval by 25% with a minimum of 1 day; an API error uses the last forecast up to 48 hours old; no forecast older than 48 hours applies no adjustment; a timeout is handled the same as an error; each city is fetched once even when several users share it. The FCM handler is covered by a passing test. Use placeholders only. Run `cd functions && npm test`, `npx jest`, `npx tsc --noEmit`, `npx eslint . --max-warnings=0` and the verify skill; paste the output. Constraints: weather calls only from Cloud Functions; do not edit SPEC.md; do not run firebase deploy. Stop after 30 turns and list anything unfinished.
```
## Phase 5: monetisation
**Scope:** AdMob with test ad unit IDs in dev. RevenueCat with entitlement `premium` and a paywall. Ad placement rules from SPEC.md. Diary with side-by-side growth photos and a date slider for premium users.
**Files likely touched:** `src/features/paywall/`, `src/features/diary/`, `src/lib/ads/`, `src/lib/purchases/`, `firestore.rules`, `storage.rules`, tests.
**Manual checks (real device):**
- Buy in the iOS sandbox and on a Google Play test track. Restore purchases.
- Confirm test ads show in the dev build.
- Confirm no ads appear after purchase.
- Confirm no ad shows during onboarding or on notification tap.
**/goal:**
```text
/goal Phase 5 monetisation is done when tests pass for: the premium entitlement gates the diary and ad removal; no ad component renders when the user is premium; no ad renders during onboarding or on notification tap; interstitials show at most once every 10 minutes; the diary comparison view renders two photos side by side with a working date slider. Use test ad unit IDs only in non-production config, and the placeholder YOUR_REVENUECAT_KEY. Run `npx tsc --noEmit`, `npx eslint . --max-warnings=0`, `npx jest` and the verify skill; paste the output. Constraints: do not run eas build or eas submit; do not edit SPEC.md. Stop after 30 turns and list anything unfinished.
```
## Phase 6: release hardening
**Scope:** Complete the real-device notification checklist for iOS and Android. Firestore rules tests for every collection. Privacy labels and data safety notes. EAS production profile with placeholders. Store listing checklist. Use the release-checklist skill.
**Files likely touched:** `docs/notification-checklist.md`, `docs/privacy-and-data-safety.md`, `docs/store-listing.md`, `firestore.rules` tests, `eas.json`, `app.json` permission strings.
**Manual checks (real device):**
- Run the full notification checklist on one iOS device and one Android 13 or newer device.
- Publish a privacy policy URL and add it to the store listings.
- Ask for approval before any `eas build --profile production` or `eas submit`.
**/goal:**
```text
/goal Phase 6 release prep is done when: the verify skill passes all steps; Firestore emulator rules tests cover every collection in SPEC.md and pass; docs/notification-checklist.md, docs/privacy-and-data-safety.md and docs/store-listing.md have every section filled, with only placeholders such as YOUR_REVENUECAT_KEY left; the checklist items for the human are marked [ ]. Use placeholders only and no real keys. Constraints: do not run eas build, eas submit or firebase deploy; do not edit SPEC.md. Report any step that was skipped and why. Stop after 20 turns and list anything unfinished.
```
REVIEW.md# Review rules
Use these rules when reviewing changes. Prettier and ESLint handle formatting, so skip style comments.
## Notifications
- Any change to scheduling in `src/features/reminders/` or `functions/src/scheduling/` must keep tests for DST changes, permission denied, and the iOS 64 pending-notification limit.
- Reminder times must use the user's stored timezone, not the device's current timezone.
## Secrets and provider calls
- No secrets or API keys in code, `app.json`, or committed env files. Use placeholders such as `YOUR_API_KEY`.
- Identification and weather API calls happen only in Cloud Functions, never in the client.
## Data and rules
- Firestore and Storage access is owner-only. Every new collection or storage path needs rules and an emulator test.
- Flag unbounded Firestore reads (no `limit` or pagination).
- Flag large or unoptimised images.
## Ads and premium
- Only test ad unit IDs in non-production builds.
- Users with the `premium` entitlement never see ads.
## App quality
- Screens that fetch or upload need loading and error states.
- Camera and reminder controls need accessibility labels.
SPEC.md# Plant care app spec
Status: draft. Answer the open questions at the end before Phase 2.
## Problem and target user
Casual plant owners forget to water their plants. They do not know how often a species needs water, or how season and weather change that. The app identifies a plant from a photo and sends watering and fertilising reminders that adjust to species, season and local weather.
Target user: casual owners with 3 to 20 houseplants or balcony plants who use a phone every day.
## MVP user stories
- US1: Sign in with email link or Google. Offer Sign in with Apple on iOS if Google is offered.
- US2: Add a plant by photo. The user takes or picks a photo and gets a species suggestion.
- US3: Confirm or correct the species.
- US4: See the care schedule for the plant.
- US5: Receive watering and fertilising reminders.
- US6: Mark a reminder as done.
- US7: Snooze a reminder.
## Acceptance criteria
US1 Sign in
- A new user gets a `users/{uid}` document with `timezone` and `city` fields.
- Signed-in state survives an app restart (emulator test).
- Signing out cancels all local notifications for that user.
US2 Add plant by photo
- The photo uploads to `users/{uid}/plants/` in Storage.
- The identification call goes through a Cloud Function. The client never receives the provider key.
- If identification fails, the user sees an error and can retry or type a species name by hand.
US3 Confirm or correct species
- The screen shows the top suggestion and up to 4 alternatives.
- The user can search species by name and select one.
- The saved plant stores the confirmed `speciesId`, not the raw suggestion.
US4 See care schedule
- The plant screen shows water and fertiliser intervals from `careProfiles`.
- An unknown species uses a default profile (water every 7 days, fertiliser every 30 days) and shows a note that it is a default.
- Next water and fertiliser dates appear in the user's local time.
US5 Receive reminders
- A watering reminder fires at the due time in the user's timezone (default 09:00).
- Reminders are scheduled as local notifications for the next 30 to 50 due items.
- Tapping a notification opens that plant's screen and shows no ad.
US6 Mark done
- Marking done schedules the next reminder from today using the current interval.
- Marking done works offline. The Firestore write syncs when the device is back online.
US7 Snooze
- Snooze offers 1 day, 3 days and 1 week.
- A snoozed reminder does not fire before `snoozedUntil`.
- Snoozing cancels the pending local notification and schedules a new one.
## Reminder logic
| Input | Source | Rule |
|---|---|---|
| Base water interval | `careProfiles.waterIntervalDays` | Starting interval |
| Base fertiliser interval | `careProfiles.fertiliserIntervalDays` | Starting interval, 0 means none |
| Season multiplier | City latitude and current month | Winter x1.5, spring x1.0, summer x0.8, autumn x1.2 (placeholder values, flip for southern hemisphere) |
| Rain (outdoor plants) | Forecast precipitation of 5 mm or more in next 24 h | Delay the watering reminder by 1 day, once per cycle |
| Heat wave | Forecast max of 32 C or more for 3 days | Shorten the water interval by 25%, minimum 1 day |
| Due time | User setting | Default 09:00 in the user's timezone |
Light and humidity are shown on the plant screen but do not change the calculation in v1.
## Weather
- City-based forecast from a free API (key placeholder: `YOUR_WEATHER_API_KEY`).
- A scheduled Cloud Function refreshes the forecast daily, once per city.
- If the weather API fails, use the last forecast up to 48 hours old. If none exists, apply no weather adjustment.
## Free vs paid
- Free: banner ads and interstitial ads. Interstitials show at most once every 10 minutes, after a completed action. Never during onboarding or on notification tap.
- Paid (entitlement `premium`): no ads. Unlocks the diary, with growth photos side by side and a date slider.
## Data model
- `users/{uid}`: timezone, city, createdAt, notificationPermission.
- `plants/{plantId}`: ownerUid, name, speciesId, photoPath, outdoor (bool), createdAt.
- `careProfiles/{speciesId}`: waterIntervalDays, fertiliserIntervalDays, light, humidity, source. Read by signed-in users, written only by Cloud Functions or admins.
- `reminders/{reminderId}`: plantId, ownerUid, type (water or fertilise), dueAt (UTC), localDueDate, status (scheduled, done, snoozed), snoozedUntil, localNotificationId.
- `diaryEntries/{entryId}`: plantId, ownerUid, photoPath, takenAt, note.
Every user-owned document has `ownerUid` (or sits under `users/{uid}`). Rules allow owners only.
## Notification reliability
- Delivery target: a due reminder shows within 15 minutes of its due time, in 95% of cases, on a device with permission granted.
- Permission denied: the app still works. Due items appear in an in-app list, and a banner explains how to enable notifications. Ask again at most once every 30 days.
- Reboot: iOS keeps pending local notifications. On Android, reschedule on app open. Missed items stay in the in-app list.
- Reinstall: reminders restore from Firestore at sign-in and are rescheduled.
- Timezone change: recompute the local schedule on app open and store the new timezone.
- Offline: local reminders fire without network. Writes queue until connection returns.
- Pending limit: keep 30 to 50 pending notifications (iOS allows 64). Re-fill on app open and on FCM data message.
## Non-goals for v1
- Social features
- Plant marketplace
- Disease diagnosis
## Open questions
- Which plant identification provider?
- Which care data source for species profiles?
- Subscription pricing and trial length?
- Which languages at launch?