{
  "slug": "firebase",
  "name": "Firebase",
  "summary": "Google's hosted Firestore database and Firebase Authentication, protected with Firestore security rules.",
  "audience": [
    "builders",
    "developers"
  ],
  "bestFor": "Teams already on Google Cloud, or sites that also have a mobile app.",
  "cost": "Free Spark plan for small sites; pay as you go on Blaze.",
  "rank": 3,
  "covers": "full",
  "docs": [
    {
      "label": "Firebase Authentication",
      "href": "https://firebase.google.com/docs/auth"
    },
    {
      "label": "Firestore security rules",
      "href": "https://firebase.google.com/docs/firestore/security/get-started"
    }
  ],
  "tested": "Tested 21 October 2026 in the Firebase emulators: 63 security-rule checks across every role, plus the sign-in and bookings screens end to end. Server-side session cookies aren't covered by the test yet.",
  "body": "## 1. Collections\n\nUse one Firestore collection per table in the data model, with the same field names in camelCase: `staffMembers` (document id = the user's Firebase Auth uid), `settings` (one document, id `main`), `bookings`, `orders`, `enquiries`, `menuSections`, `menuItems`, `customers`, `reviews`, `openingHours` (ids `0`–`6`), `closures`. Store money as whole pence and times as Firestore timestamps.\n\n## 2. Security rules\n\nDeploy these as `firestore.rules`. They encode the access table; every read and write goes through them.\n\n```\nrules_version = '2';\nservice cloud.firestore {\n  match /databases/{db}/documents {\n    function member() { return get(/databases/$(db)/documents/staffMembers/$(request.auth.uid)).data; }\n    function signedIn() { return request.auth != null; }\n    function onTeam() { return signedIn() && exists(/databases/$(db)/documents/staffMembers/$(request.auth.uid)); }\n    function isManager() { return onTeam() && member().role in ['owner', 'manager']; }\n    function isOwner() { return onTeam() && member().role == 'owner'; }\n    function onlyChanges(fields) { return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields); }\n\n    match /menuSections/{id} { allow read: if true; allow write: if isManager(); }\n    match /menuItems/{id}    { allow read: if true; allow write: if isManager(); }\n    match /openingHours/{id} { allow read: if true; allow write: if isManager(); }\n    match /closures/{id}     { allow read: if true; allow write: if isManager(); }\n    match /settings/{id}     { allow read: if true; allow write: if isOwner(); }\n\n    match /bookings/{id} {\n      allow create: if request.resource.data.status == 'confirmed'\n        && request.resource.data.keys().hasOnly(['startsAt', 'name', 'party', 'phone', 'email', 'note', 'status', 'createdAt']);\n      allow read: if onTeam();\n      allow update: if isManager() || (onTeam() && onlyChanges(['status']));\n      allow delete: if isManager();\n    }\n    match /orders/{id} {\n      allow create: if request.resource.data.stage == 'new' && request.resource.data.paid == false;\n      allow read: if onTeam();\n      allow update: if isManager() || (onTeam() && onlyChanges(['stage']));\n      allow delete: if isManager();\n    }\n    match /enquiries/{id} {\n      allow create: if request.resource.data.status == 'new';\n      allow read, update, delete: if isManager();\n    }\n    match /customers/{id} { allow read, write: if isManager(); }\n    match /reviews/{id}   { allow read, write: if isManager(); }\n    match /staffMembers/{uid} {\n      allow read: if signedIn() && (request.auth.uid == uid || isManager());\n      allow write: if isOwner();\n    }\n  }\n}\n```\n\nThe `onlyChanges` checks mean staff can move a booking to \"arrived\" or an order to \"ready\", but can't edit anything else.\n\n## 3. Make the first owner\n\nCreate the owner in Authentication → Users, copy their UID, and add a document `staffMembers/<uid>` with `{ email, name, role: \"owner\", status: \"active\" }` in the Firestore console. Rules can't create the first owner, so this step is manual on purpose.\n\n## 4. Protect /admin on the server\n\nFirebase Auth lives in the browser by default. For server protection in Next.js, exchange the ID token for a session cookie after sign-in (`admin.auth().createSessionCookie(idToken, { expiresIn })` in a server route), then in middleware or each admin layout call `admin.auth().verifySessionCookie(cookie, true)` and redirect to `/admin/sign-in` when it fails. The Firebase Admin SDK credentials stay in server environment variables only. On Next.js 16 the file is `proxy.ts` (function `proxy`); on 15 and earlier, `middleware.ts`.\n\n## 5. Wire the screens\n\n- admin-sign-in: `signInWithEmailAndPassword`, and `sendSignInLinkToEmail` for the email link.\n- Lists: `getDocs`/`onSnapshot` on the collection; saves: `updateDoc`/`setDoc`. The rules decide what each role may do.\n- admin-staff invites: create the user with the Admin SDK in a server route and write the `staffMembers` document.",
  "url": "https://copypasteui.dev/r/recipes/firebase.json",
  "prompt": "# Back office with Firebase\n\nSet up the back office for this site with this backend, following every step. The back-office screens are the CopyPasteUI \"admin\" components (admin-sign-in, admin-overview, admin-bookings …); each takes its data and save actions as props. Best for: Teams already on Google Cloud, or sites that also have a mobile app. Cost: Free Spark plan for small sites; pay as you go on Blaze.\n\n## The back-office data model\n\nEvery recipe stores the same data with the same names, so the back-office screens (category \"admin\") work with any backend. Money is stored in pence/cents as whole numbers. Times are stored in UTC.\n\n| Table | Fields | Used by |\n| --- | --- | --- |\n| `staff_members` | user_id (the auth user), email, name, role (`owner` \\| `manager` \\| `staff`), status (`active` \\| `invited`) | admin-staff, every access check |\n| `settings` | one row: name, phone, email, address, instagram, booking_link, notify_bookings, notify_enquiries, notify_reviews, daily_summary | admin-settings, the public header/footer/contact |\n| `bookings` | id, starts_at, name, party, phone, email, table_label, note, status (`confirmed` \\| `arrived` \\| `no-show` \\| `cancelled`), created_at | admin-bookings, admin-overview |\n| `orders` | id, number, customer_name, placed_at, pickup_at, items (list of {name, qty}), total_pence, paid, stage (`new` \\| `preparing` \\| `ready` \\| `collected`) | admin-orders |\n| `enquiries` | id, name, email, phone, subject, message, received_at, status (`new` \\| `replied` \\| `archived`) | admin-enquiries |\n| `menu_sections` | id, name, position | admin-menu-editor, the public menu |\n| `menu_items` | id, section_id, name, description, price_pence, tags (list), available, position | admin-menu-editor, the public menu |\n| `customers` | id, name, email, phone, visits, last_visit, spend_pence, notes, marketing_opt_in | admin-customers |\n| `reviews` | id, source, author, rating (1–5), text, posted_at, reply | admin-reviews |\n| `opening_hours` | day (0 = Monday … 6 = Sunday), open, opens, closes | admin-hours, the public footer/contact |\n| `closures` | id, starts_on, ends_on, note | admin-hours, the closed-notice banner |\n\n## Who may do what\n\nThis is the security contract. Implement it in the database's own rules, not only in the interface — hiding a button doesn't stop anyone.\n\n| Data | Public visitors | Staff | Manager | Owner |\n| --- | --- | --- | --- | --- |\n| menu_sections, menu_items, opening_hours, closures | read | read | read, write | read, write |\n| settings | read (public fields only) | read | read | read, write |\n| bookings | create only (from the booking form), never read | read, change status | read, write | read, write |\n| orders | create only (from checkout), never read | read, change stage | read, write | read, write |\n| enquiries | create only (from the contact form), never read | — | read, write | read, write |\n| customers, reviews | — | — | read, write | read, write |\n| staff_members | — | read own row | read | read, write |\n\nRules that apply to every backend:\n- Every `/admin` page checks the session **on the server** and sends signed-out people to `/admin/sign-in`.\n- Secret keys (service-role keys, admin SDK credentials, API secrets) live only in server code and environment variables, never in the browser bundle.\n- Public forms may only insert, with fields limited to what the form sends; they can never read other people's rows.\n- `/admin` is left out of the navigation and the sitemap, and disallowed in robots.txt.\n- The first owner account is created by hand once (instructions in each recipe); everyone else is invited from admin-staff.\n\n# Firebase: steps\n\n## 1. Collections\n\nUse one Firestore collection per table in the data model, with the same field names in camelCase: `staffMembers` (document id = the user's Firebase Auth uid), `settings` (one document, id `main`), `bookings`, `orders`, `enquiries`, `menuSections`, `menuItems`, `customers`, `reviews`, `openingHours` (ids `0`–`6`), `closures`. Store money as whole pence and times as Firestore timestamps.\n\n## 2. Security rules\n\nDeploy these as `firestore.rules`. They encode the access table; every read and write goes through them.\n\n```\nrules_version = '2';\nservice cloud.firestore {\n  match /databases/{db}/documents {\n    function member() { return get(/databases/$(db)/documents/staffMembers/$(request.auth.uid)).data; }\n    function signedIn() { return request.auth != null; }\n    function onTeam() { return signedIn() && exists(/databases/$(db)/documents/staffMembers/$(request.auth.uid)); }\n    function isManager() { return onTeam() && member().role in ['owner', 'manager']; }\n    function isOwner() { return onTeam() && member().role == 'owner'; }\n    function onlyChanges(fields) { return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields); }\n\n    match /menuSections/{id} { allow read: if true; allow write: if isManager(); }\n    match /menuItems/{id}    { allow read: if true; allow write: if isManager(); }\n    match /openingHours/{id} { allow read: if true; allow write: if isManager(); }\n    match /closures/{id}     { allow read: if true; allow write: if isManager(); }\n    match /settings/{id}     { allow read: if true; allow write: if isOwner(); }\n\n    match /bookings/{id} {\n      allow create: if request.resource.data.status == 'confirmed'\n        && request.resource.data.keys().hasOnly(['startsAt', 'name', 'party', 'phone', 'email', 'note', 'status', 'createdAt']);\n      allow read: if onTeam();\n      allow update: if isManager() || (onTeam() && onlyChanges(['status']));\n      allow delete: if isManager();\n    }\n    match /orders/{id} {\n      allow create: if request.resource.data.stage == 'new' && request.resource.data.paid == false;\n      allow read: if onTeam();\n      allow update: if isManager() || (onTeam() && onlyChanges(['stage']));\n      allow delete: if isManager();\n    }\n    match /enquiries/{id} {\n      allow create: if request.resource.data.status == 'new';\n      allow read, update, delete: if isManager();\n    }\n    match /customers/{id} { allow read, write: if isManager(); }\n    match /reviews/{id}   { allow read, write: if isManager(); }\n    match /staffMembers/{uid} {\n      allow read: if signedIn() && (request.auth.uid == uid || isManager());\n      allow write: if isOwner();\n    }\n  }\n}\n```\n\nThe `onlyChanges` checks mean staff can move a booking to \"arrived\" or an order to \"ready\", but can't edit anything else.\n\n## 3. Make the first owner\n\nCreate the owner in Authentication → Users, copy their UID, and add a document `staffMembers/<uid>` with `{ email, name, role: \"owner\", status: \"active\" }` in the Firestore console. Rules can't create the first owner, so this step is manual on purpose.\n\n## 4. Protect /admin on the server\n\nFirebase Auth lives in the browser by default. For server protection in Next.js, exchange the ID token for a session cookie after sign-in (`admin.auth().createSessionCookie(idToken, { expiresIn })` in a server route), then in middleware or each admin layout call `admin.auth().verifySessionCookie(cookie, true)` and redirect to `/admin/sign-in` when it fails. The Firebase Admin SDK credentials stay in server environment variables only. On Next.js 16 the file is `proxy.ts` (function `proxy`); on 15 and earlier, `middleware.ts`.\n\n## 5. Wire the screens\n\n- admin-sign-in: `signInWithEmailAndPassword`, and `sendSignInLinkToEmail` for the email link.\n- Lists: `getDocs`/`onSnapshot` on the collection; saves: `updateDoc`/`setDoc`. The rules decide what each role may do.\n- admin-staff invites: create the user with the Admin SDK in a server route and write the `staffMembers` document.\n\nDocs to check before you start, since providers change their APIs: Firebase Authentication (https://firebase.google.com/docs/auth); Firestore security rules (https://firebase.google.com/docs/firestore/security/get-started).\n"
}
