๐Ÿ—“๏ธ
Last Updated: 2026-07-03

Full v2 rebuild: API connected, real auth, config-driven. First deployment: Castle Checkers (caretaker portal, Naples FL).

What This Is

A white-label service management portal for small service businesses. SONAN builds, hosts and maintains it. Clients use it via a subdomain of sonandigital.com and pay a monthly subscription managed in the SONAN CRM.

The same codebase serves completely different businesses โ€” lawn care, caretakers, realtors, dog groomers โ€” by swapping configuration. No per-client code changes needed.

Architecture

LayerWhatPer-client?
FrontendReact + TypeScript (Vite)One Cloudflare Pages project per client, all built from same main branch
APICloudflare Worker (TypeScript)One Worker per client (npx wrangler deploy --env <client>)
DatabaseCloudflare D1 (SQLite)One D1 per client โ€” full data isolation
ConfigD1 settings table + Cloudflare Pages env varsPer-client env vars in Pages dashboard; settings editable via admin UI
AuthPBKDF2 password hash in D1 admin_users table, HMAC-signed tokensEach client has their own admin password, changeable from admin UI

Repo

ItemValue
GitHubgithub.com/sonantechai/caretaker-portal (rename to service-portal)
LocalE:\Claude_Projects\sonan-trackers\caretaker-portal-Claude\caretaker-portal\
Cloudflare accountsonantechai
Branch strategySingle main branch only โ€” no per-client branches

Current Clients

ClientBusiness TypeSubdomainWorkerD1 DBStatus
Castle CheckersCaretaker / Home Watchcastlecheckers.sonandigital.comcastlecheckers-apicaretaker_dbโณ Ready to deploy
Realtor (TBD)Real EstateTBD.sonandigital.comrealtor-apirealtor_db๐Ÿ”œ Planned

What Changes Per Client (No Code Change Needed)

SettingHow to ConfigureExamples
Business name, phone, email, addressD1 settings table โ†’ editable in admin Settings pageCastle Checkers vs ABC Lawn
Location textD1 settings table"Naples, Florida" vs "Austin, Texas"
Service types (wizard options)D1 settings table (JSON array)Airport Pickup vs Lawn Mowing vs Buyer Inquiry
Task checklist itemsD1 settings table (JSON array)Home watch tasks vs yard tasks
Invoice footer / notesD1 settings tablePer-client messages
Primary colorD1 settings table (hex)#2d6a4f (green) vs #1565c0 (blue)
Admin passwordD1 admin_users table โ€” changeable via admin Settings pagePer-client

File Structure

caretaker-portal/ (service portal template)
  src/
    App.tsx                    โ† Root: loads config on mount, shows PublicSite or AdminApp
    api.ts                     โ† All fetch() calls โ€” single source of truth
    index.css                  โ† Complete responsive CSS
    context/
      AppContext.tsx            โ† State management: real API calls, no local state mutation
    components/
      PublicSite.tsx            โ† Public site โ€” config-driven (no hardcoded text)
      ServiceRequestWizard.tsx  โ† Customer request form โ€” service types from DB
      AdminApp.tsx              โ† Admin shell + sidebar nav
      admin/
        Dashboard.tsx
        RequestsView.tsx        โ† Requests inbox, internal notes, convert-to-client
        ClientsView.tsx         โ† Full CRUD: add, edit, delete
        AppointmentsView.tsx    โ† Schedule management, edit, delete
        CalendarView.tsx        โ† Month/week view
        InvoicesView.tsx        โ† Line items, totals, print view
        ReportsView.tsx         โ† Revenue charts + stats
        SettingsView.tsx        โ† Business info, service types, change password
  worker/
    index.ts                   โ† Full Worker API (542 lines)
  migrations/
    0001_init.sql              โ† Schema: admin_users, clients, service_requests,
                               โ† properties, appointments, invoices, invoice_items, notes, settings
    0002_settings_seed.sql     โ† Castle Checkers seed data + password hash
  wrangler.toml                โ† castlecheckers env configured; realtor commented placeholder
  _redirects                   โ† Cloudflare Pages SPA routing

Database Schema

TablePurposeKey Columns
admin_usersAdmin loginid, name, email, password_hash (PBKDF2 hex)
clientsClient recordsfull_name, phone, email, address, emergency_contact_name/phone, status
service_requestsPublic submissionsfull_name, service_types (JSON), travel_details (JSON), home_care_details (JSON), internal_notes (JSON), status
appointmentsScheduled jobsclient_id, type, appointment_date, start_time, pickup/dropoff/property_address, flight_number, airline, airport
invoicesBillingclient_id, invoice_number, subtotal, tax, discount, total, status
invoice_itemsLine itemsinvoice_id, description, quantity, rate, amount
settingsPer-client configkey, value (string) โ€” all business config lives here
propertiesProperties per clientclient_id, address, access_instructions โ€” future use
notesEntity notesentity_type, entity_id, note โ€” future use

Worker API Routes

MethodPathAuthPurpose
GET/api/configPublicReturns all settings (business name, service types, etc.) โ€” called on page load
POST/api/requestsPublicCustomer submits service request via wizard
POST/api/admin/loginPublicReturns HMAC-signed Bearer token (24h)
GET/PUT/api/admin/requests[/:id]BearerList all / update status + internal notes
GET/POST/PUT/DELETE/api/admin/clients[/:id]BearerFull CRUD
GET/POST/PUT/DELETE/api/admin/appointments[/:id]BearerFull CRUD; joins client name
GET/POST/PUT/api/admin/invoices[/:id]BearerFull CRUD with invoice_items; joins client name
GET/POST/api/admin/settingsBearerRead/write all settings keys
POST/api/admin/passwordBearerChange admin password (PBKDF2 re-hash + store in D1)

Authentication

No npm packages โ€” uses Web Crypto API (built into Cloudflare Workers runtime).

WhatHow
Password storagePBKDF2-SHA256, salt SONAN_SP_2026, 100k iterations โ†’ hex string in D1 admin_users.password_hash
Password changeVia admin Settings page โ†’ POST /api/admin/password โ†’ re-hash + store in D1
Token creationLogin: verify hash โ†’ create base64(payload).HMAC-SHA256-signature using JWT_SECRET Worker secret
Token verificationEvery protected route: split token, verify HMAC, check expiry
Token lifetime24 hours. Client re-logs in after expiry.

Adding a New Client (Checklist)

  1. Create D1 database: npx wrangler d1 create <clientname>_db
  2. Add [env.clientname] section to wrangler.toml with new DB ID
  3. Run migration: npx wrangler d1 migrations apply <clientname>_db --remote --env <clientname>
  4. Create a migration 000N_<clientname>_seed.sql with their business info in settings table
  5. Run that migration too
  6. Set Worker secret: npx wrangler secret put JWT_SECRET --env <clientname> (random string)
  7. Deploy worker: npx wrangler deploy --env <clientname>
  8. Create Cloudflare Pages project pointing to sonantechai/caretaker-portal main branch
  9. Set Pages env var: VITE_API_URL = deployed Worker URL
  10. Add custom domain: <clientname>.sonandigital.com โ†’ Pages project