Service Portal
Service Portal — Architecture & Context
Last updated: 2026-07-12
Last Updated: 2026-07-07
Castle Checkers live. 8 enhancements added: invoice delete, mobile client modal, contact strip layout, service request edit styling, appointment auto-switch, migrations 0003–0006, blocked days API.
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 | ✅ Live — July 2026 |
| 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 towrangler.tomlwith new DB ID - Run migration:
npx wrangler d1 migrations apply <clientname>_db --remote --env <clientname> - Create a migration
000N_<clientname>_seed.sqlwith 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-portalmain branch - Set Pages env var:
VITE_API_URL= deployed Worker URL - Add custom domain:
<clientname>.sonandigital.com→ Pages project