Last updated: 2026-07-12

Stack

LayerTechnology
FrontendReact 18 + TypeScript — Vite 5 — Cloudflare Pages
Backend APICloudflare Worker (TypeScript) — env: bernies
DatabaseCloudflare D1 — lawncare_db
AuthPBKDF2-SHA256 password hash in D1 · HMAC-signed JWT (24 hr expiry)
PDF generationjsPDF (browser-side) — invoked in EstimatesView and InvoicesView
Mobile layoutPosition-fixed sidebar drawer + hamburger toggle — mobile-first CSS

Database Schema

TableKey Columns
admin_usersid (UUID), email, password_hash, password_changed (bool)
clientsid, name, email, phone, service_address, city, zip, status, notes, created_at
employeesid, name, email, phone, role, created_at
jobsid, client_id, employee_id, service_type, scheduled_date, scheduled_time, duration_minutes, address, price_cents, status, recurrence, recurrence_days, notes, created_at
estimatesid, estimate_number, client_id, services, total_cents, status, valid_until, notes, created_at
invoicesid, invoice_number, client_id, job_id, amount_cents, description, status, issue_date, paid_date, created_at
invoice_itemsid, invoice_id, description, quantity, unit_price_cents, total_cents
paymentsid, invoice_id, amount_cents, method, paid_date, notes
service_requestsid, name, email, phone, services, message, status, created_at
blocked_daysid, type (single/range/weekday), date, start_date, end_date, weekdays (JSON), reason, created_at
settingskey, value — stores all business config as key-value pairs
login_attemptsid, ip, attempted_at — used for rate limiting (5 attempts / 15 min)

Settings Keys

KeyDefault Value
business_nameBernie's Backyard Manicured Lawncare
business_phone(555) 555-5555
business_emailinfo@example.com
business_address123 Main St, Anytown USA
business_logo(empty — base64 data URL when set)
service_types["Lawn Mowing","Edging & Trimming","Fertilisation","Aeration","Leaf Removal","Hedge Trimming","Snow Removal","Irrigation Check"]
invoice_footerThank you for choosing Bernie's Backyard!
invoice_next_number1001
estimate_next_number2001

Auth Flow

Login POST → Worker verifies PBKDF2 hash → issues HMAC-signed JWT (24 hr) → frontend stores in sessionStorage under key lawncare_token. Every authenticated API call sends Authorization: Bearer <token>. Rate limiting: 5 failed attempts per IP per 15 minutes, tracked in login_attempts. The password_changed flag on admin_users drives the default-password banner in the UI.

File Structure

lawncare-portal/
  src/
    App.tsx                         ← Root: token check → PublicSite or AdminApp
    api.ts                          ← All fetch() wrappers — reads VITE_API_URL
    index.css                       ← Mobile-first CSS (drawer, hamburger, badges, cards)
    main.tsx                        ← Vite entry point
    context/
      AppContext.tsx                 ← Global state — auth, all data, config, document.title
    components/
      PublicSite.tsx                 ← Config-driven public landing page + estimate request form
      AdminApp.tsx                   ← Sidebar drawer · hamburger · login screen · user-guide iframe
      admin/
        Dashboard.tsx                ← Stat cards + recent jobs panel
        JobsView.tsx                 ← Job CRUD + modal form + status flow
        CalendarView.tsx             ← Month calendar + click-to-add-job
        ClientsView.tsx              ← Client CRUD + job history tab
        EmployeesView.tsx            ← Employee CRUD
        EstimatesView.tsx            ← Estimate CRUD + PDF (jsPDF) + email + convert to invoice
        InvoicesView.tsx             ← Invoice CRUD + line items + PDF (jsPDF) + email + print
        ServiceRequestsView.tsx      ← Inbound requests from public site
        ReportsView.tsx              ← Monthly revenue chart + KPI panels
        SettingsView.tsx             ← Business settings + logo + blocked days + password change
  public/
    user-guide.html                  ← Embedded admin user guide (iframe target)
  worker/
    index.ts                         ← All API routes (single file)
  migrations/
    0001_init.sql                    ← Full schema — all tables + indices
    0002_settings_seed.sql           ← Default settings + admin user (Admin@2026 hash)
    0003_extras.sql                  ← login_attempts · blocked_days · password_changed · recurrence columns · logo setting
  wrangler.toml                      ← [env.bernies] — D1 binding + custom domain
  vite.config.ts
  tsconfig.json
  package.json

API Routes

MethodRouteAuthNotes
GET/api/configNonePublic settings for landing page (business_name, service_types, etc.)
POST/api/requestsNoneSubmit estimate request from public form → inserts into service_requests
POST/api/auth/loginNoneVerify password → return JWT; rate-limited by IP
GET/POST/api/jobsJWTList all jobs or create new
PUT/DELETE/api/jobs/:idJWTUpdate or delete a job
GET/POST/api/clientsJWTList all clients or create new
PUT/DELETE/api/clients/:idJWTUpdate or delete a client
GET/POST/api/employeesJWTList all employees or create new
PUT/DELETE/api/employees/:idJWTUpdate or delete an employee
GET/POST/api/estimatesJWTList all estimates or create new
PUT/DELETE/api/estimates/:idJWTUpdate or delete an estimate
GET/POST/api/invoicesJWTList all invoices or create new (auto-increments invoice_next_number)
PUT/DELETE/api/invoices/:idJWTUpdate or delete (delete blocked if status = Paid)
POST/api/invoices/:id/itemsJWTAdd line item to invoice
DELETE/api/invoices/:id/items/:itemIdJWTRemove line item
GET/POST/api/service-requestsJWTList all requests or create manually
PUT/api/service-requests/:idJWTUpdate status / notes
GET/api/settingsJWTReturn all settings as key-value map
PUT/api/settingsJWTUpsert one or more setting keys
PUT/api/auth/change-passwordJWTVerify current pw, update hash, set password_changed = 1
GET/POST/api/blocked-daysJWTList or create blocked day entries
DELETE/api/blocked-days/:idJWTRemove a blocked day entry

CORS & Security

Worker sets permissive CORS headers (Access-Control-Allow-Origin: *) to allow the Pages frontend to call the Worker API. In production, this is safe because the JWT protects all mutation endpoints and the Worker is not accessible to anonymous write operations except the public /api/requests and /api/config routes. Login rate limiting (5 attempts / 15 min per IP) is enforced in the Worker before password comparison.

Frontend State

All data lives in AppContext (React context). On login, the context fetches all resources in parallel (jobs, clients, employees, estimates, invoices, service requests, settings, blocked days) and stores them in state. Mutations call the API then re-fetch or optimistically update local state. The admin view is a single-page app — AdminApp.tsx renders the active view by adminView string with no router library. The user-guide view renders an <iframe src="/user-guide.html"> filling the content area.