108 lines
7.4 KiB
Markdown
108 lines
7.4 KiB
Markdown
# Phase 2, Sub-project A — Backend Foundation Design
|
|
|
|
## Context
|
|
|
|
Phase 1 shipped a fully static "Braids by Mira" site: all product/service/availability data lives in local TypeScript modules (`src/data/*.ts`), and the booking/inquiry flows only transition UI state — nothing is persisted or sent anywhere. Phase 2 replaces this with a real backend so Mira can manage her own products, services, and availability, and so client bookings/inquiries actually reach her.
|
|
|
|
Phase 2 is too large for a single spec, so it's split into three sequential sub-projects, each independently shippable:
|
|
|
|
- **Sub-project A (this spec):** backend foundation — database, auth, storage, notification functions. Seeded with the current mock data; the existing frontend is untouched and keeps working exactly as it does today.
|
|
- **Sub-project B (future spec):** admin panel — login, CRUD for products/services/availability, viewing bookings and inquiries.
|
|
- **Sub-project C (future spec):** public site goes live — Shop/Book/Rules read real data, bookings and inquiries actually persist through to the backend built here.
|
|
|
|
This spec only covers standing up the backend itself. No existing UI changes.
|
|
|
|
## Tech Stack
|
|
|
|
- **Firestore** — the database, for products, services, the availability schedule, bookings, and inquiries.
|
|
- **Firebase Auth** (email/password) — a single admin account for Mira, provisioned manually via the Firebase console. No public sign-up flow.
|
|
- **Firebase Storage** — product/service/portrait images, eventually replacing the static files under `public/`.
|
|
- **Cloud Functions** — two Firestore-triggered functions (no HTTP endpoints) that keep a public availability view in sync and send Mira email notifications.
|
|
- **Resend** — transactional email API used by the Cloud Functions to notify Mira of new bookings/inquiries. Free tier (3,000 emails/month) is more than sufficient.
|
|
- **Cost note:** Cloud Functions making outbound network calls (to Resend) require Firebase's Blaze (pay-as-you-go) plan rather than the free Spark plan. Blaze has a generous free tier for a site this size, but requires a billing method attached to the Firebase project — a manual setup step outside this codebase.
|
|
|
|
## Data Model
|
|
|
|
The central design constraint: the public site must be able to determine which appointment times are taken without ever being able to read who booked them.
|
|
|
|
```
|
|
products/{id}
|
|
name: string
|
|
category: 'hair-care' | 'extensions' | 'tools' | 'accessories'
|
|
price: number
|
|
image: string // Storage path, e.g. "products/edge-control.jpg"
|
|
description: string
|
|
inStock: boolean
|
|
|
|
services/{id}
|
|
name: string
|
|
durationMinutes: number
|
|
priceFrom: number
|
|
description: string
|
|
image: string // Storage path, e.g. "styles/knotless.jpg"
|
|
|
|
scheduleTemplate/default
|
|
workingDays: number[] // 0=Sunday..6=Saturday, e.g. [2,3,4,5,6] for Tue-Sat
|
|
startTime: string // "09:00"
|
|
endTime: string // "18:00"
|
|
slotMinutes: number // e.g. 120
|
|
|
|
scheduleExceptions/{date} // date = "YYYY-MM-DD"
|
|
closed: boolean
|
|
note?: string // e.g. "Vacation"
|
|
|
|
bookings/{id} // ADMIN-ONLY READ — contains PII
|
|
serviceId: string
|
|
date: string // "YYYY-MM-DD"
|
|
time: string // "10:00 AM"
|
|
contact: { name: string, phone: string, email: string, notes: string }
|
|
status: 'confirmed' | 'cancelled'
|
|
createdAt: Timestamp
|
|
|
|
bookedSlots/{date} // PUBLIC READ — no PII, mirrors bookings
|
|
times: string[] // e.g. ["10:00 AM", "1:00 PM"]
|
|
|
|
inquiries/{id} // ADMIN-ONLY READ — contains PII
|
|
productId: string
|
|
contact: { name: string, email: string, notes: string }
|
|
status: 'new' | 'read'
|
|
createdAt: Timestamp
|
|
```
|
|
|
|
**Why `bookedSlots` is separate from `bookings`:** the public Book page needs to know which times are already taken for a given date, but must never be able to read the associated names/phone numbers/emails of other clients. `bookedSlots/{date}` is a small, PII-free public mirror containing only the booked times. The real record with contact info lives in `bookings`, which only Mira can read. A Cloud Function keeps the mirror in sync — the client never writes to `bookedSlots` directly, so there's no way to spoof or bypass it from the browser.
|
|
|
|
## Security Rules
|
|
|
|
**Firestore:**
|
|
- `products`, `services`, `scheduleTemplate`, `scheduleExceptions`, `bookedSlots`: public read, admin-only write.
|
|
- `bookings`, `inquiries`: public **create** only (so the public booking/inquiry forms can submit without being signed in), admin-only read/update/delete.
|
|
- "Admin-only" is checked via `request.auth.uid == '<Mira's fixed Firebase Auth UID>'` — a single hardcoded UID constant in the rules file. No roles/permissions system is needed for exactly one admin user.
|
|
|
|
**Storage:**
|
|
- `products/*`, `styles/*`, `about/*`: public read, admin-only write.
|
|
|
|
## Cloud Functions
|
|
|
|
Both are Firestore-triggered background functions — no HTTP endpoints are exposed, so there's no public API surface to secure beyond the Firestore/Storage rules above.
|
|
|
|
- **`onBookingCreated`** (trigger: `bookings/{id}` onCreate) — reads the new booking's `date` and `time`, appends `time` to `bookedSlots/{date}.times` (creating the document if it doesn't exist yet, using an array-union style merge so concurrent bookings on the same day don't clobber each other), then sends an email to Mira via Resend containing the service, date, time, and client contact details.
|
|
- **`onInquiryCreated`** (trigger: `inquiries/{id}` onCreate) — sends an email to Mira via Resend containing the product name and client contact details.
|
|
|
|
## Client Integration
|
|
|
|
- Add the `firebase` npm package to the existing Vite app.
|
|
- New `src/lib/firebase.ts` initializes the Firebase app from environment variables and exports configured `db` (Firestore), `auth`, and `storage` instances for later sub-projects to import.
|
|
- Firebase config values (API key, project ID, etc.) are read from `.env.local`, which is gitignored — never committed. A `.env.example` documents the required variable names with placeholder values.
|
|
|
|
## Seeding & Verification
|
|
|
|
- A one-time seed step populates Firestore with the current mock data: all 10 products, all 4 services, and a default `scheduleTemplate` matching the existing mock availability pattern (Tue-Sat, working hours, 2-hour slots) — so the backend is populated and testable immediately, without waiting for sub-project B's admin UI to exist.
|
|
- Since sub-project A doesn't touch the existing frontend, verification happens outside the app: confirming Firestore reads/writes work as expected (e.g. via the Firebase console or a small throwaway script), confirming the security rules actually block unauthorized access (a client without admin auth cannot read `bookings`/`inquiries` or write to `products`/`services`), and confirming both Cloud Functions fire correctly end-to-end (creating a test booking document triggers the `bookedSlots` update and a real email arrives via Resend).
|
|
|
|
## Out of Scope (this sub-project)
|
|
|
|
- Any change to the existing public site UI or `src/data/*.ts` — those stay exactly as they are until sub-project C.
|
|
- The admin panel UI itself (login screen, product/service/availability management screens, bookings/inquiries dashboard) — sub-project B.
|
|
- Wiring the public Book/Shop pages to actually read from Firestore or write real bookings/inquiries — sub-project C.
|
|
- Any booking-cancellation flow beyond the `status` field existing on the `bookings` document — cancellation UI is an admin-panel concern for sub-project B.
|