๐๏ธ
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
| Layer | What | Per-client? |
| Frontend | React + TypeScript (Vite) | One Cloudflare Pages project per client, all built from same main branch |
| API | Cloudflare Worker (TypeScript) | One Worker per client (npx wrangler deploy --env <client>) |
| Database | Cloudflare D1 (SQLite) | One D1 per client โ full data isolation |
| Config | D1 settings table + Cloudflare Pages env vars | Per-client env vars in Pages dashboard; settings editable via admin UI |
| Auth | PBKDF2 password hash in D1 admin_users table, HMAC-signed tokens | Each client has their own admin password, changeable from admin UI |
Repo
| Item | Value |
| GitHub | github.com/sonantechai/caretaker-portal (rename to service-portal) |
| Local | E:\Claude_Projects\sonan-trackers\caretaker-portal-Claude\caretaker-portal\ |
| Cloudflare account | sonantechai |
| Branch strategy | Single main branch only โ no per-client branches |
Current Clients
| Client | Business Type | Subdomain | Worker | D1 DB | Status |
| Castle Checkers | Caretaker / Home Watch | castlecheckers.sonandigital.com | castlecheckers-api | caretaker_db | โณ Ready to deploy |
| Realtor (TBD) | Real Estate | TBD.sonandigital.com | realtor-api | realtor_db | ๐ Planned |
What Changes Per Client (No Code Change Needed)
| Setting | How to Configure | Examples |
| Business name, phone, email, address | D1 settings table โ editable in admin Settings page | Castle Checkers vs ABC Lawn |
| Location text | D1 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 items | D1 settings table (JSON array) | Home watch tasks vs yard tasks |
| Invoice footer / notes | D1 settings table | Per-client messages |
| Primary color | D1 settings table (hex) | #2d6a4f (green) vs #1565c0 (blue) |
| Admin password | D1 admin_users table โ changeable via admin Settings page | Per-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
| Table | Purpose | Key Columns |
| admin_users | Admin login | id, name, email, password_hash (PBKDF2 hex) |
| clients | Client records | full_name, phone, email, address, emergency_contact_name/phone, status |
| service_requests | Public submissions | full_name, service_types (JSON), travel_details (JSON), home_care_details (JSON), internal_notes (JSON), status |
| appointments | Scheduled jobs | client_id, type, appointment_date, start_time, pickup/dropoff/property_address, flight_number, airline, airport |
| invoices | Billing | client_id, invoice_number, subtotal, tax, discount, total, status |
| invoice_items | Line items | invoice_id, description, quantity, rate, amount |
| settings | Per-client config | key, value (string) โ all business config lives here |
| properties | Properties per client | client_id, address, access_instructions โ future use |
| notes | Entity notes | entity_type, entity_id, note โ future use |
Worker API Routes
| Method | Path | Auth | Purpose |
| GET | /api/config | Public | Returns all settings (business name, service types, etc.) โ called on page load |
| POST | /api/requests | Public | Customer submits service request via wizard |
| POST | /api/admin/login | Public | Returns HMAC-signed Bearer token (24h) |
| GET/PUT | /api/admin/requests[/:id] | Bearer | List all / update status + internal notes |
| GET/POST/PUT/DELETE | /api/admin/clients[/:id] | Bearer | Full CRUD |
| GET/POST/PUT/DELETE | /api/admin/appointments[/:id] | Bearer | Full CRUD; joins client name |
| GET/POST/PUT | /api/admin/invoices[/:id] | Bearer | Full CRUD with invoice_items; joins client name |
| GET/POST | /api/admin/settings | Bearer | Read/write all settings keys |
| POST | /api/admin/password | Bearer | Change admin password (PBKDF2 re-hash + store in D1) |
Authentication
No npm packages โ uses Web Crypto API (built into Cloudflare Workers runtime).
| What | How |
| Password storage | PBKDF2-SHA256, salt SONAN_SP_2026, 100k iterations โ hex string in D1 admin_users.password_hash |
| Password change | Via admin Settings page โ POST /api/admin/password โ re-hash + store in D1 |
| Token creation | Login: verify hash โ create base64(payload).HMAC-SHA256-signature using JWT_SECRET Worker secret |
| Token verification | Every protected route: split token, verify HMAC, check expiry |
| Token lifetime | 24 hours. Client re-logs in after expiry. |
Adding a New Client (Checklist)
- Create D1 database:
npx wrangler d1 create <clientname>_db
- Add
[env.clientname] section to wrangler.toml with new DB ID
- Run migration:
npx wrangler d1 migrations apply <clientname>_db --remote --env <clientname>
- Create a migration
000N_<clientname>_seed.sql with their business info in settings table
- Run that migration too
- Set Worker secret:
npx wrangler secret put JWT_SECRET --env <clientname> (random string)
- Deploy worker:
npx wrangler deploy --env <clientname>
- Create Cloudflare Pages project pointing to
sonantechai/caretaker-portal main branch
- Set Pages env var:
VITE_API_URL = deployed Worker URL
- Add custom domain:
<clientname>.sonandigital.com โ Pages project