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 |
menu/{itemId}¶
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.