TL;DR
HydroBid is a Capacitor + React app for pressure washing / soft wash estimates. Quotes and appointments live in IndexedDB (Dexie). PDFs are generated on-device. Calendar reminders use local notifications. No account required for core quoting.
Product page: knoxstudios.net/apps/hydrobid.html
This post is a build log: architecture choices, freemium edges, and Android gotchas—not a feature dump dressed as a tutorial.
Why this app exists
Exterior cleaning crews often bid from the driveway. Cloud-only estimators fail when signal fails. We wanted:
- Line items with sq ft / linear ft / hour / flat
- Branded PDF share from the phone
- Pipeline + calendar so follow-ups don’t die in SMS
- Wash helpers: chemical mix, area, water usage
- Local-first data (Dexie), Capacitor for Android
Architecture (high level)
┌─────────────────────────────────────────┐
│ React 19 + Vite + Tailwind (UI) │
│ QuoteBuilder · Calendar · Tools · PDF │
├─────────────────────────────────────────┤
│ Dexie / IndexedDB │ session drafts │
│ quotes, appts, │ photo compress │
│ settings │ │
├─────────────────────────────────────────┤
│ Capacitor 8 │
│ Camera · Filesystem · Share │
│ Local Notifications · AdMob · RC IAP │
└─────────────────────────────────────────┘
Source of truth = device. AdMob and RevenueCat are side channels; they must never block reading/writing quotes.
Quote model (simplified)
type QuoteStatus = 'draft' | 'sent' | 'accepted' | 'rejected'
interface LineItem {
id: string
label: string
qty: number
unit: 'sq_ft' | 'linear_ft' | 'hour' | 'flat'
rate: number
amount: number
}
interface Quote {
id: string
clientAddress: string
lineItems: LineItem[]
photoDataUrls: string[]
status: QuoteStatus
price: number
// ... notes, validUntil, paymentTypes, sentAt, appointmentAt, etc.
}
Always normalize on read: default empty arrays, recompute price from line items. Field apps accumulate messy rows after updates; defensive normalization > perfect migrations alone.
PDF pipeline
- Map
Quote+ company settings →@react-pdf/rendererdocument (themes: canopy / invoice / simple) - Optionally downscale photos for the PDF (we cap embedded images)
- On native: write file → Share plugin
- Enforce free-tier unique export quota per month in one module so UI and paywall agree
Lesson: treat the PDF as the customer API. If Share works offline after generation, you’ve won the job-site demo.
Calendar without a backend
- Store
Appointmentrows locally (optionalquoteId) - Derive calendar markers from quote timestamps (
sentAt,respondedAt,validUntil) + appointments - Export
.icsvia Filesystem/Share or open a Google Calendar URL when online - Rebuild local notifications from DB (T-24h / T-2h for jobs; nudges for drafts/unsent/expiry)
- Respect quiet hours + max notifications per day so you don’t train users to disable everything
Android 13+ needs POST_NOTIFICATIONS. Exact alarms need the right permission storytelling in the OS settings screen—prime this in onboarding, don’t ambush post-install.
Tools that write back to the draft
Chemical dilution, L×W area, and GPM×time water math live under a Tools tab. Persisting calculator drafts (and “add to quote” actions) keeps wash-specific math inside the same session as the bid.
Small UX win, large retention win: users stop alt-tabbing to random calculator apps.
Freemium edges we encoded early
| Free | Premium |
|---|---|
| Ads + PDF generate/share gate | Ad-free |
| Limited unique PDF exports / month | Unlimited exports |
| Up to 2 job photos / quote | Up to 6 |
| Basic dashboard | Service-mix insights |
| “Made with HydroBid” on PDF | Clean footer + rate hide/customize |
Encode limits as constants (FREE_EXPORT_UNIQUE_QUOTES_PER_MONTH, FREE_MAX_QUOTE_PHOTOS). Scattered magic numbers create support tickets.
Onboarding that earns permission
Order that worked for us:
- Welcome / problem framing
- How it works (quote → PDF → follow-up)
- Company profile (logo + default rate)
- Notifications prime (why reminders matter)
- Soft Premium intro (no hard wall)
Profile-before-paywall means the first PDF already looks like their business.
What I’d tell another indie shipping field tools
- Ship offline path first; sync is a sequel.
- Compress images before IndexedDB.
- One module owns quotas and Premium checks.
- Local notifications need a rebuild function you can call after every meaningful write.
- Be explicit in store copy about what you don’t do (tax, maps, live CRM)—it reduces 1★ “where’s X?” reviews.
Links
- App / product page: HydroBid
- Stack: React 19, Vite, Capacitor 8, Dexie,
@react-pdf/renderer, RevenueCat, AdMob
Questions about Dexie schema versioning or PDF photo embedding? Drop them in the comments—happy to compare notes with other Capacitor folks shipping trades apps.
Top comments (0)