API & Data
This page documents the API client layer, all backend endpoints, error handling, the Proof-of-Work engine, client-side storage, and utility libraries. For i18n details, see Architecture.
API Client
Source: src/lib/api/client.ts
The HTTP client is a thin wrapper over fetch. Three exported functions — apiGet, apiPost, apiDelete — delegate to a shared request<T>() helper:
- Constructs the full URL via
apiUrl(path)(see config.ts). - Sends the request with
Content-Type: application/json. - On non-2xx: throws
ApiErrorwith a typedcode(never returns error bodies). - On success: returns the parsed JSON body.
Key detail: parseBody first reads the response as text, then attempts JSON.parse. If parsing fails, it returns undefined — this handles empty 204-style responses gracefully.
API Endpoints
Source: src/lib/api/booking.ts
All paths are relative to /v1/book. The single URL construction point is src/lib/config.ts (apiUrl()).
Booking Flow
Manage Flow
Key Response Types
ProfileResponse—timezone,currency(fallbackUSD),hoursJson,otpMethods(['sms','email']),logoUrl,name,address,phone,email.AvailabilityResponse—date,slots[](UTC ISO strings),timezone.ManageView—status,start,end,services[],timezone,currency,canCancel,canReschedule.CreateBookingResponse—bookingId,status: 'pending',holdExpiresAt,manageToken.PowChallenge—challenge,signature,difficultyBits.
Error Handling
Source: src/lib/api/errors.ts
All API errors are thrown as ApiError instances with a typed code property. Always branch on ApiError.code, not the HTTP status.
Error Code Categories
Body-driven codes (read from the response body's error field, snake_case normalized):
Status-driven codes (inferred from HTTP status when no body error matches):
The KNOWN_BODY_CODES set in errors.ts is the authoritative list of which error strings the backend sends in the body. Adding a new backend error code requires updating both the ApiErrorCode union and KNOWN_BODY_CODES.
Proof-of-Work Engine
Source: src/lib/pow/
PoW protects the booking API from abuse by requiring clients to solve a hashcash challenge before calling sensitive endpoints (createHold, requestOtp).
Pipeline
PowProvider(PowProvider.tsx) — React context that wraps the booking wizard. Caches solved tokens for 240s (server TTL is ~300s). Deduplicates concurrent callers with a single in-flightPromise.getPow()— Fetches a{ challenge, signature, difficultyBits }triple fromPOST /pow.solvePowAuto(solveAuto.ts) — Dispatches solving to a Web Worker if available, falling back to the synchronoussolvePowon the main thread.solver.worker.ts— Web Worker entry point. Receives{ challenge, difficultyBits }, runssolvePow, posts back{ nonce }.solvePow(solve.ts) — Brute-force solver: iterates nonce integers, computesSHA-256(challenge|nonce), checks leading zero bits againstdifficultyBits.leadingZeroBits(leadingZeroBits.ts) — Bit-counting utility for the hash output.
Usage
Components call usePowToken().getFreshPow(force?) to get a { challenge, signature, nonce } triple. The wizard pre-warms a token during pickTime so that holding and verifyContact don't block on solving. On invalid_proof_of_work or bad_request rejection, callers retry once with getFreshPow(force: true).
Client-Side Storage
Source: src/lib/storage.ts
A single localStorage key (salon.bookings) stores a JSON array of SavedBooking objects:
saveBooking(b)— appends to the array.listBookings()— reads the array; returns[]on parse failure or missing key.
Saved bookings are used on the HomePage to show a "My bookings" list with links to /manage/:token. This is purely a convenience feature — the manage token is the sole credential; localStorage is never used for auth.
Contact Validation
Source: src/lib/contact/validate.ts
Supports two contact channels:
- Email — basic regex validation (
isValidEmail). - Phone — US/CA NANP only. Functions:
normalizeNanp(s)— strip to 10-digit national number, dropping leading country-code1.isValidNanpPhone(s)— validates area code and exchange first digits are 2–9 (NXX-NXX-XXXX).formatNanpInput(raw)— progressive(555) 223-4567input mask.formatNanpDisplay(s)— display format for stored numbers.
Time Utilities
Calendar (src/lib/time/calendar.ts)
Pure functions operating on YYYY-MM-DD date strings (never UTC instants, never browser-local zone):
formatYMD,parseYMD— convert betweenDateandYYYY-MM-DD.addDays(ymd, n)— arithmetic on calendar dates.upcomingDays(startYmd, count)— generate a list of consecutive dates.monthGrid(year, month0)— 42-cell Monday-first calendar grid.
Business Hours (src/lib/time/hours.ts)
Parses hoursJson (JSON object keyed by ISO weekday 1=Mon..7=Sun, mapping to { open, close } in minutes since midnight):
parseHours(json)— parse and validate the JSON string.weeklyHours(parsed)— normalize into a 7-elementDayHours[]array.formatMinutes(min)— format minutes-since-midnight as 12-hour time.
Timezone Display (src/lib/time/tz.ts)
Converts UTC ISO instants to the salon's IANA timezone for display. All output uses US conventions:
- Times: 12-hour format (
9:00 AM). - Dates:
Jun 21, 2026. formatSlotTime(utcIso, timezone)— time only.formatInTz(utcIso, timezone, opts)— time with optional date prefix (e.g.,Jun 21, 2026 · 9:00 AM).
Service Grouping
Source: src/lib/services/group.ts
Groups a flat ServiceItem[] list by categoryId. Unclassified services (categoryId: null) appear first as "Featured." Remaining groups are sorted by categorySortOrder (ascending, nulls last). This is used by PickService to render the categorized service list with a <ChipBar> filter.
Notes for Future Agents
- Adding a new API endpoint: Add a typed function in
src/lib/api/booking.ts. UseapiGet/apiPost/apiDeletefromclient.ts. Do not construct URLs manually — always useapiUrl(path). - Adding a new error code: Add the string to
ApiErrorCodeinerrors.ts. If the backend sends it in the response body, also add it toKNOWN_BODY_CODES. - Changing the API base URL: Only
src/lib/config.tsconstructs URLs. No other file should referenceimport.meta.env.VITE_BOOKING_BASE_URLdirectly. - PoW solving changes: The solver intentionally runs on the main thread as a fallback. If changing the Web Worker, test both worker and non-worker paths (
typeof Worker === 'undefined'in environments that lack it). - LocalStorage: The
salon.bookingskey is the only data stored client-side. It's never used for authentication — the manage token from the URL is the sole credential.