ShoeSafari stores the public product catalog and private commerce details in
Firestore. Collection and field names below reflect the current client code.
Firestore document IDs are generated by Firestore for new products and orders;
the order's orderId is a separate human-readable identifier.
The storefront reads every document in ShoeSafariProducts. The admin product
forms create and update these fields:
| Field | Firestore type | Required for current UI | Meaning |
|---|---|---|---|
name |
string | Yes | Product display name. |
price |
number | Yes | Product price in USD, in major units (for example, 79.99). It is a catalog value, not a trusted checkout total. |
img |
string | Yes | Firebase Storage download URL used for the product image. |
available |
boolean, optional | No | Whether customers can buy the product. Only false means unavailable; legacy records without this field remain available. New products default to true. |
The product document ID is used as the catalog product ID and in the checkout quote. Do not put customer, payment, or order data in product documents. The quote endpoint reads catalog prices server-side and calculates the payable total; browser-submitted prices and totals are untrusted.
lib/orderStore.js creates an order after a successful Stellar payment. The
fields below describe new records; Firestore serverTimestamp() values resolve
to Firestore timestamps after the write completes.
| Field | Firestore type | Required on new records | Meaning |
|---|---|---|---|
orderId |
string | Yes | Human-readable order identifier, also represented by the on-chain order hash. |
paymentMethod |
string | Yes | Currently "stellar". |
status |
string | Yes | Commerce-side mirror of the observed on-chain status (Paid at creation, later Shipped or Refunded). See status authority below. |
userId |
string or null |
Yes | Firebase Auth UID for a signed-in buyer; null for guest checkout. It is the ownership key for customer order queries. |
amountUsd |
number | Yes | USD-denominated checkout amount for display. |
amountRaw |
string | Yes | Integer token amount in the token's smallest units. Stored as a decimal string to preserve integer precision. |
transactionHash |
string | Yes | Stellar transaction hash for the payment. |
ledger |
number or null |
Yes | Ledger from the payment receipt when available. |
buyerWallet |
string or null |
Yes | Stellar address reported by the payment receipt when available. |
tokenContract |
string or null |
Yes | Token contract address from the receipt when available. |
orderIdHash |
string or null |
Yes | Order identifier hash from the receipt when available. |
customer |
map | Yes | Delivery/contact details described below. |
items |
array of maps | Yes | Purchased line items; see current and legacy shapes below. |
createdAt |
timestamp | Yes | Server-generated creation time. |
updatedAt |
timestamp | Yes | Server-generated creation/update time. Updated when the admin view mirrors a status change. |
completedAt |
timestamp | Added on completion | Server timestamp written when status becomes Shipped or Refunded; starts the personal-data retention period. Legacy records use updatedAt as the completion time when available. |
personalDataAnonymizedAt |
timestamp | Optional | Server timestamp marking removal of direct customer identifiers from this Firestore copy. |
The customer map currently contains firstName, lastName, email, and
address, each a string. These values are copied from checkout form state; the
application currently writes an empty string if a value is absent. Treat them
as private personal information. Collect and retain only fields needed to
fulfill the order, limit access to the merchant and the owning customer, and
never add card number, expiry, CVV, OTP, or other payment credentials to this
document. The card inputs in the checkout UI are not part of the Stellar order
record. Retain direct customer fields and the Firebase userId for one year
(365 days) after completion (Shipped or Refunded), then use the admin
orders page to anonymize them. A verified customer deletion request may be
honored earlier. The action clears the customer map, userId, and copied
buyerWallet; it records personalDataAnonymizedAt and leaves order IDs,
transaction hashes, amounts, status, and line items intact for accounting.
The order page continues to show the buyer address from the public chain index;
this action does not erase or change that public chain data.
New checkout quote line items use this shape:
| Field | Firestore type | Meaning |
|---|---|---|
productId |
string | Product document ID. |
name |
string | Product name captured at checkout. |
quantity |
number | Purchased quantity. |
unitPriceUsd |
number | Server-confirmed unit price in USD. |
totalUsd |
number | Server-confirmed line total in USD. |
If quote line items are unavailable, the checkout currently falls back to the
browser's saved cart items. Older records may therefore contain id, name,
price, and img instead of the quote fields. Consumers should tolerate
missing item fields and both shapes; do not assume cart values in a legacy
record were server-verified.
The Soroban escrow contract and its indexed events are authoritative for
payment and escrow state. The Firestore status is denormalized commerce
metadata for customer/admin display and may be absent, stale, or out of sync;
it is not proof that funds settled. Dispatch and refund actions must be
authorized and verified on-chain. Firestore status writes only mirror a
successful on-chain action.
Customer order reads must be restricted to documents whose userId equals the
authenticated Firebase UID. Guest records (userId: null) do not have an
authenticated customer owner and must not be exposed through a broad public
query. Merchant/admin access should be granted through server-managed
authorization, not only the client-side email allowlist. Product catalog reads
may be public; product writes and order reads/updates must be protected by
Firestore Security Rules or a trusted server.
Readers must tolerate old documents with missing optional metadata, absent
timestamps, legacy cart item fields, and missing status; show an unavailable
value or derive display state from on-chain events rather than failing the
whole order view. Do not infer ownership when userId is missing. Before
changing a required field, collection name, or item shape, deploy readers that
accept both old and new forms, then backfill records with an idempotent,
reviewed migration. Keep original document IDs and transaction hashes stable;
never rewrite chain-derived facts based only on Firestore values. No migration
is required for this documentation update.
The checkout quote API reads product prices through Firebase Admin on the
server. Configure FIREBASE_SERVICE_ACCOUNT_JSON with a server-only service
account and CHECKOUT_QUOTE_SECRET with a private random secret of at least 32
characters. Never prefix these variables with NEXT_PUBLIC_ or expose them to
browser code. The API accepts product IDs and quantities, ignores prices and
totals from the browser, and returns a signed quote that expires after 10
minutes. The wallet flow verifies the signature and expiry immediately before
building the transaction. Buyers see the server-confirmed total and quote
expiry before signing.
The checked-in firestore.rules file is the authorization boundary. Product
documents are publicly readable and only writable by accounts whose Firebase
ID token contains the boolean custom claim admin: true. Customers can read
their own orders; only admins can read all orders or update/delete orders.
Guest order creation is limited to documents with a null userId. All other
paths are denied by default. The browser AdminGuard only controls navigation
and display; Firestore evaluates the signed token claim for every operation.
Grant or revoke the claim from a trusted machine with the Firebase Admin SDK
and credentials permitted to update users' claims. Create a service account
with the minimum required Firebase Authentication permissions, set
GOOGLE_APPLICATION_CREDENTIALS to its private file path, and run:
FIREBASE_PROJECT_ID=your-project node scripts/set-admin-claim.mjs grant FIREBASE_UID
FIREBASE_PROJECT_ID=your-project node scripts/set-admin-claim.mjs revoke FIREBASE_UIDThe script preserves other custom claims. Never put the service-account file in the repository or a browser bundle. After changing a claim, have the user sign in again or refresh their ID token. Removing the claim denies subsequent Firestore requests once Firebase issues the refreshed token.
Run the Firestore rules tests locally with Java installed. This is the same command used by the Firestore rules CI workflow:
npm run test:rules:firestoreThe emulator uses the demo-shoesafari demo project ID and never contacts a
production Firebase project or requires production credentials. Run both
Firestore and Storage rules suites with npm run test:rules.
Deploy the production rules separately with:
firebase deploy --only firestore:rules --config firebase.firestore.json --project your-project-id