7.4 KiB
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'sdateandtime, appendstimetobookedSlots/{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
firebasenpm package to the existing Vite app. - New
src/lib/firebase.tsinitializes the Firebase app from environment variables and exports configureddb(Firestore),auth, andstorageinstances 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.exampledocuments 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
scheduleTemplatematching 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/inquiriesor write toproducts/services), and confirming both Cloud Functions fire correctly end-to-end (creating a test booking document triggers thebookedSlotsupdate 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
statusfield existing on thebookingsdocument — cancellation UI is an admin-panel concern for sub-project B.