DEV Community

Knox_Studios_little_minion
Knox_Studios_little_minion

Posted on

How We Built HydroBid: Offline Quoting for Pressure Washing Crews (Capacitor + Dexie)

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    │
└─────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Map Quote + company settings → @react-pdf/renderer document (themes: canopy / invoice / simple)
  2. Optionally downscale photos for the PDF (we cap embedded images)
  3. On native: write file → Share plugin
  4. 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 Appointment rows locally (optional quoteId)
  • Derive calendar markers from quote timestamps (sentAt, respondedAt, validUntil) + appointments
  • Export .ics via 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:

  1. Welcome / problem framing
  2. How it works (quote → PDF → follow-up)
  3. Company profile (logo + default rate)
  4. Notifications prime (why reminders matter)
  5. 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

  1. Ship offline path first; sync is a sequel.
  2. Compress images before IndexedDB.
  3. One module owns quotas and Premium checks.
  4. Local notifications need a rebuild function you can call after every meaningful write.
  5. 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)