Skip to content

Firestore schema

Every collection MealMind reads or writes, with field types and the code that uses it.

Note

Field names are case-sensitive. Some screens read alternate keys for older data (noted below).

Overview

erDiagram
    USERS ||--o{ FAVORITES : "users/{uid}/favorites"
    USERS ||--o{ CART_ITEMS : customerId
    USERS ||--o{ ORDERS : customerId
    ORDERS ||--o{ ORDER_ITEMS : "orders/{id}/items"
    USERS ||--o{ BILL : customerId
    BILL ||--o{ PAYMENT : billId
    MENU ||--o{ CART_ITEMS : itemId
    MENU ||--o{ RATINGS : itemId
    CATEGORIES ||--o{ MENU : category
    TABLE_DETAIL ||--o| USERS : currentCustomerId

users/{uid}

Created on sign-up (lib/auth/signup_screen.dart) or by an admin (lib/admin/screens/user_list_screen.dart).

Field Type Notes
name string
email string
role string "user", "chef" or "admin". Missing = regular user. Checked by AuthGuard and the admin login.
phone string Optional, shown on the profile page
wantsOffers bool From the sign-up checkbox
tableNumber, floor string Optional, read by getTableStream()

users/{uid}/favorites/{itemId}

Field Type
addedAt timestamp

Seed data: firebase/seed/menu.json. Managed in lib/admin/screens/menu_list_screen.dart.

Field Type Example
itemId string "combo_north_indian_thali" (same as doc ID)
itemName string "North Indian Thali"
description string
price number 360
kcal number 760
serves string "1–2"
prepTime string "30 min"
image string URL (Google Drive share links are converted by fixGoogleDriveLink)
isAvailable bool
ingredients array\<string>
category string Category ID, e.g. "Combos"
likes number Optional, used for popularity sorting

categories/{categoryId}

Seed data: firebase/seed/categories.json.

Field Type
categoryId string
name string
description string
image string (URL)

tableDetail/{id}

Create one per physical table. Used by lib/user/screens/qr_table_scan_screen.dart.

Field Type Notes
tableNumber string Must match what the QR code contains
label string Optional display name, defaults to Table <n>
isOccupied bool Set to true on check-in
currentCustomerId string UID of the seated customer

cartItems/{autoId}

One document per cart line. Managed by CartProvider and CartScreen.

Field Type
customerId string (UID)
itemId string
itemName string
imageUrl string
price number
quantity int
tableNumber string (optional)

orders/{order_<millis>}

Created by OrderService.placeOrderAndGoToStatus (lib/user/services/order_service.dart).

Field Type Notes
orderId string Same as doc ID
customerId string
status string Customer app writes placed; kitchen moves it through preparing → ready → completed. The kitchen queue treats the legacy value received like placed.
createdAt timestamp Server timestamp
isCompleted bool
isPaid bool
paymentId, paidAt string, timestamp Set after Razorpay success
tableNumber string or int Both are parsed by the admin screen
totalAmount number Sum of price × quantity

orders/{orderId}/items/{autoId}

A copy of each cartItems document at the time of ordering (itemId, itemName, price, quantity, …). The admin screen also accepts legacy name / qty keys and an optional note.

counters/bill_counter

Must exist before the first bill is generated.

Field Type
lastBillNum int (start at 0)

Incremented in a transaction by generateNextBillIdAtomic() to produce bill_0001, bill_0002, …

bill/{billId}

Created by createBillAndCleanupAtomic() in lib/user/screens/order_status_screen.dart once every active order is ready. The merged orders are deleted afterwards.

Field Type Notes
billId string
customerId string
orderIdList array\<string> Orders merged into this bill
tableNumber string
itemBreakdown array\<{itemId, quantity, price}> Same items across orders are merged
subtotal number
tax number 5% of subtotal
discount number Currently always 0
total number subtotal + tax − discount
paidStatus string unpaid → paid
generatedAt timestamp
paidAt timestamp After payment

payment/{autoId}

Written after a successful Razorpay payment (lib/user/screens/razorpay_payment_screen.dart).

Field Type
paymentId, razorpayId string
orderId, billId string
customerId string
tableNumber string
amount number
paymentStatus "paid"
method "online"
date timestamp

ratings/{autoId}

Field Type
itemId string
rating int (1–5)

Composite indexes

These queries need composite indexes. Firestore prints a one-click creation link in the debug console the first time each one runs.

Collection Fields Used by
orders customerId ↑, createdAt ↓ Order status screen
bill customerId ↑, generatedAt ↓ Past bills screen
cartItems customerId ↑, itemId ↑ Cart updates
menu category ↑, price ↑/↓, itemName ↑, likes ↓ Menu filter + sort

Security rules (starter)

The repository does not ship rules yet. This is a starting point, not a complete policy. Test it with the Firebase Emulator before relying on it.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function signedIn() { return request.auth != null; }
    function isAdmin() {
      return signedIn() &&
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }
    function owns(data) { return signedIn() && data.customerId == request.auth.uid; }

    match /users/{uid} {
      allow read: if signedIn() && (request.auth.uid == uid || isAdmin());
      // Users can edit their profile but never their own role.
      allow create: if signedIn() && request.auth.uid == uid && !('role' in request.resource.data);
      allow update: if (signedIn() && request.auth.uid == uid &&
                        !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']))
                    || isAdmin();
      allow delete: if isAdmin();

      match /favorites/{itemId} {
        allow read, write: if signedIn() && request.auth.uid == uid;
      }
    }

    match /menu/{id}       { allow read: if true;     allow write: if isAdmin(); }
    match /categories/{id} { allow read: if true;     allow write: if isAdmin(); }
    match /tableDetail/{id} { allow read, update: if signedIn(); allow create, delete: if isAdmin(); }

    match /cartItems/{id} {
      allow read, update, delete: if owns(resource.data);
      allow create: if owns(request.resource.data);
    }

    match /orders/{id} {
      allow read: if owns(resource.data) || isAdmin();
      allow create: if owns(request.resource.data);
      allow update, delete: if owns(resource.data) || isAdmin();
      match /items/{itemId} { allow read, write: if signedIn(); }
    }

    match /bill/{id} {
      allow read: if owns(resource.data) || isAdmin();
      allow create: if owns(request.resource.data);
      allow update: if owns(resource.data) || isAdmin();
    }

    match /counters/{id} { allow read, update: if signedIn(); }
    match /payment/{id}  { allow create: if signedIn(); allow read: if isAdmin(); }
    match /ratings/{id}  { allow read: if true; allow create: if signedIn(); }
  }
}

Client-written totals

Because bills and payment status are written by the client, a determined user can still forge them under these rules. Moving bill generation and Razorpay verification into Cloud Functions is the real fix.