@base44/app-plugin-commerce 0.1.20 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +25 -22
  2. package/base44/agents/commerce/StoreAdmin.jsonc +1 -1
  3. package/base44/entities/commerce.OrderRefund.jsonc +1 -1
  4. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  5. package/base44/entities/commerce.Webhook.jsonc +1 -1
  6. package/base44/functions/commerce/admin-products/entry.ts +1 -1
  7. package/base44/functions/commerce/admin-reports/entry.ts +1 -1
  8. package/base44/functions/commerce/payments/entry.ts +2 -2
  9. package/base44/functions/commerce/seed-store/defaults.ts +1 -1
  10. package/base44/functions/commerce/seed-store/entry.ts +34 -0
  11. package/base44/functions/commerce/seed-store/seed-catalog.ts +39 -5
  12. package/base44/functions/commerce/storefront-catalog/entry.ts +1 -1
  13. package/base44/functions/commerce/storefront-checkout/entry.ts +1 -1
  14. package/base44/shared/commerce/card-payment.stripe.ts +198 -0
  15. package/base44/shared/commerce/card-payment.ts +1 -1
  16. package/base44/shared/commerce/payments.ts +2 -2
  17. package/base44/shared/commerce/scan.ts +1 -1
  18. package/base44/shared/commerce/sequence.ts +2 -2
  19. package/package.json +1 -1
  20. package/scripts/install.js +24 -14
  21. package/skills/commerce/SKILL.md +117 -51
  22. package/skills/commerce/docs/api-admin.md +89 -28
  23. package/skills/commerce/docs/api-storefront.md +113 -126
  24. package/skills/commerce/docs/entities.md +137 -0
  25. package/skills/commerce/install/01-install.md +101 -0
  26. package/skills/commerce/install/02-storefront.md +444 -0
  27. package/skills/commerce/install/03-data.md +162 -0
  28. package/skills/commerce/references/admin-product-form.md +10 -0
  29. package/skills/commerce/references/catalog-rendering.md +110 -0
  30. package/skills/commerce/references/emails.md +49 -12
  31. package/skills/commerce/references/guest-access-security.md +18 -5
  32. package/skills/commerce/references/online-payments.md +49 -149
  33. package/skills/commerce/references/operations.md +52 -0
  34. package/skills/commerce/references/reviews.md +31 -16
  35. package/skills/commerce/references/shipping-and-tax.md +110 -0
  36. package/skills/commerce/references/store-admin-agent.md +21 -0
  37. package/skills/commerce/references/store-settings.md +49 -0
  38. package/src/commerce/admin/README.md +6 -3
  39. package/src/commerce/admin/layout/AuthGuard.jsx +1 -1
  40. package/src/commerce/admin/pages/products/Reviews.jsx +1 -1
  41. package/src/commerce/admin/pages/settings/InventorySettings.jsx +1 -1
  42. package/src/commerce/admin/pages/settings/PaymentsSettings.jsx +1 -1
  43. package/src/commerce/storefront/StorefrontProvider.jsx +106 -20
  44. package/src/commerce/storefront/index.js +74 -24
  45. package/src/commerce/storefront/internal/useAsyncData.js +86 -0
  46. package/src/commerce/storefront/useAddressForm.js +96 -0
  47. package/src/commerce/storefront/useCartLine.js +221 -0
  48. package/src/commerce/storefront/useCheckout.jsx +18 -6
  49. package/src/commerce/storefront/useOrderReturn.js +36 -10
  50. package/src/commerce/storefront/useProduct.js +295 -0
  51. package/src/commerce/storefront/useProductGallery.js +74 -0
  52. package/src/commerce/storefront/useProductList.js +153 -0
  53. package/src/commerce/storefront/useProductPrice.js +58 -0
  54. package/src/commerce/storefront/useProductReviews.js +242 -0
  55. package/src/commerce/storefront/useStorefrontSeo.js +204 -0
  56. package/src/commerce/storefront/useTotalsLines.js +109 -0
  57. package/src/commerce/utils/address-spec.js +89 -0
  58. package/src/commerce/utils/images.js +45 -0
  59. package/src/commerce/utils/index.js +22 -7
  60. package/src/commerce/utils/price.js +95 -0
  61. package/src/commerce/utils/shipping-promos.js +2 -2
  62. package/src/commerce/utils/specs.js +26 -0
  63. package/src/commerce/utils/storefront.js +47 -3
  64. package/src/commerce/utils/totals.js +110 -0
  65. package/src/commerce/utils/variants.js +58 -3
  66. package/skills/commerce/installation-guidelines.md +0 -93
  67. package/skills/commerce/post-installation.md +0 -496
  68. package/skills/commerce/references/limits-and-performance.md +0 -16
  69. package/skills/commerce/references/media-and-downloads.md +0 -4
  70. package/skills/commerce/references/product-render.md +0 -89
  71. package/skills/commerce/references/scheduled-work.md +0 -19
  72. package/skills/commerce/references/storefront-product-page.md +0 -83
  73. package/skills/commerce/references/webhooks.md +0 -10
@@ -0,0 +1,137 @@
1
+ # Entities — the data-model map
2
+
3
+ Every `commerce.*` entity, what holds what, and who may write it. Read this before **any** direct entity read or write; function-mediated flows never need it.
4
+
5
+ ## Addressing — read this first
6
+
7
+ Entity names are **dotted and namespaced**, so the SDK accessor is **bracket syntax, always**:
8
+
9
+ ```js
10
+ base44.entities["commerce.PaymentGateway"] // ✅ the only spelling that resolves
11
+ base44.entities["commerce__PaymentGateway"] // ❌ 404 Entity schema … not found
12
+ base44.entities["PaymentGateway"] // ❌ there is no un-namespaced alias
13
+ base44.entities.commerce.PaymentGateway // ❌ not a nested object
14
+ ```
15
+
16
+ Schema files live at **`base44/entities/commerce.<Name>.jsonc`** — one targeted read when you need a field this page doesn't carry.
17
+
18
+ `filter` is **exact-match only**; args are `(query, sort, limit, skip)`, `list` is `(sort, limit, skip)`. Plus `.get(id)` · `.create(fields)` · `.update(id, patch)` · `.delete(id)`.
19
+
20
+ ```js
21
+ const gateways = await base44.entities["commerce.PaymentGateway"].list("order", 100);
22
+ const [offline] = await base44.entities["commerce.PaymentGateway"].filter({ slug: "offline" }, undefined, 1);
23
+ await base44.entities["commerce.PaymentGateway"].update(offline.id, { title: "Bank transfer" });
24
+ ```
25
+
26
+ **Never** scan `base44/entities/` to discover names, guess a spelling and retry on 404, or web-search a kit API's shape. This page plus [`api-admin.md`](./api-admin.md) and [`api-storefront.md`](./api-storefront.md) are the answer.
27
+
28
+ ## The catalog — all 20
29
+
30
+ **RLS is identical on every entity: `admin` on read *and* write**, catalog included. A non-admin call is rejected by the backend whatever the UI does; storefront access goes exclusively through `commerce/storefront-*` (service role, safe projections) — [`../references/guest-access-security.md`](../references/guest-access-security.md). Hence no RLS column: assume admin-only.
31
+
32
+ **Owned by `<fn>` = never write directly** — the function maintains derived fields, counts, emails and webhooks a raw write skips. Names below are all `commerce/<name>`.
33
+
34
+ | Entity | Purpose | Key fields | Write path |
35
+ |---|---|---|---|
36
+ | `Product` | Sellable product; sells variants when it carries `attributes` — there is **no `type` field** | `name` `slug` `status` `price` `category_ids` `attributes[]` `images[]` | owned by **`admin-products`** |
37
+ | `ProductVariation` | One purchasable attribute combination of a parent | `product_id` `attributes[]` `sku` `regular_price` `stock_quantity` `status` | owned by **`admin-products`** — `save` diffs them inside the parent call |
38
+ | `ProductCategory` | Hierarchical category (`parent_id` tree) | `name` `slug` `parent_id` `menu_order` `count` | direct CRUD, or **`admin-products` `save-term`** (slug uniqueness, self-parent guard) |
39
+ | `ProductRibbon` | Flat cross-cutting label ("Best Seller") | `name` `count` | direct CRUD, or **`save-term`** — get-or-create by name, so it can't split a ribbon |
40
+ | `ProductAttribute` | Variant axis shared across products (Color, Size) | `name` `code` `order` | direct CRUD, or **`save-term`** (`code` uniqueness; `code` is what a filter URL carries) |
41
+ | `ProductAttributeTerm` | One value of an attribute (Size → M) | `attribute_id` `name` `order` `count` | direct CRUD, or **`save-term`** — products store values **by name**, so a rename rewrites them |
42
+ | `ProductReview` | Moderated customer review | `product_id` `status` `reviewer` `rating` `review` `verified` | owned by **`admin-reviews`** (recomputes the rating); created by `submit-review` |
43
+ | `Order` | Order with embedded line items, shipping/tax/fee/coupon lines, totals | `order_number` `order_key` `status` `line_items[]` `total` `billing` | owned by **`admin-orders`**; created by `place-order` |
44
+ | `OrderNote` | System, private, or customer-facing note | `order_id` `note` `is_customer_note` `added_by` | owned by **`admin-orders`** `add-note`/`delete-note` — a customer note emails the buyer |
45
+ | `OrderRefund` | Full or partial refund | `order_id` `amount` `line_items[]` `restock_items` | owned by **`admin-refunds`** (totals, restock, status) |
46
+ | `Customer` | Customer; guests have no `user_id` | `email` `first_name` `last_name` `is_guest` `billing` `shipping` | owned by **`admin-customers`**; checkout upserts it, `update-my-addresses` is the owner's path |
47
+ | `Coupon` | Discount coupon with restrictions and usage limits | `code` `enabled` `discount_type` `amount` `usage_count` | owned by **`admin-coupons`** (unique lowercased code) |
48
+ | `Cart` | Ephemeral cart + checkout draft keyed by `cart_token`; items hold **no prices** | `cart_token` `items[]` `coupon_codes` `shipping_address` `expires_at` | owned by **`storefront-cart`/`-checkout`** — never write directly |
49
+ | `PaymentGateway` | One checkout method (`offline`, `card`, manual options); no secrets here | `slug` `title` `description` `enabled` `order` `settings` | **mixed**: `enabled` via `seed-store` `payment_methods`; the rest direct CRUD |
50
+ | `ShippingTaxLocation` | A sales location: regions + its shipping rates, tax groups, shipping tax. Empty `regions` = catch-all | `name` `order` `regions[]` `shipping_rates[]` `tax_groups[]` | **direct CRUD** (or `seed-store` `locations`) — mint stable rate `id`s |
51
+ | `StoreSettings` | Config, **one record per `group_id`** (`general` `products` `inventory` `tax` `shipping` `emails`) | `group_id` `values` | **direct CRUD** — patch `values`, never replace the group |
52
+ | `DownloadPermission` | A customer's access to a purchased file | `order_id` `customer_email` `product_id` `downloads_remaining` `access_expires` | owned by the **order lifecycle** (`admin-orders` `grant-download`/`revoke-download`) |
53
+ | `Webhook` | Outgoing webhook definition | `name` `topic` `delivery_url` `secret` `status` | **direct CRUD** (+ `admin-webhooks` `test`/`redeliver`); `failure_count` engine-owned |
54
+ | `WebhookDelivery` | Log of one delivery attempt | `webhook_id` `topic` `response_code` `success` | **engine-owned, read-only**; prune via `admin-tools` |
55
+ | `EmailLog` | Log of transactional emails sent | `email_type` `recipient` `target` `success` `error` | **engine-owned, read-only** |
56
+
57
+ ## Relationships
58
+
59
+ ```
60
+ Product ──< ProductVariation one row per stocked combination
61
+ ├─ category_ids ─> ProductCategory ──< ProductCategory (parent_id tree)
62
+ ├─ ribbon_ids ─> ProductRibbon
63
+ ├─ attributes[].attribute_id ─> ProductAttribute ──< ProductAttributeTerm
64
+ └──< ProductReview
65
+
66
+ Cart ──place-order──> Order ──< OrderNote
67
+ (cart_token, 48h TTL) │ ──< OrderRefund
68
+ │ ──< DownloadPermission
69
+ └─ customer_id ─> Customer
70
+
71
+ Coupon referenced by code from Cart.coupon_codes / Order.coupon_lines
72
+ StoreSettings one record per group_id PaymentGateway one row per method
73
+ ShippingTaxLocation standalone; matched by `order` ascending at pricing time
74
+ Webhook ──< WebhookDelivery EmailLog append-only
75
+ ```
76
+
77
+ ## Derived fields — never write these
78
+
79
+ | Field | Changed instead by |
80
+ |---|---|
81
+ | variant parent's `price` `regular_price` `on_sale` — rolled up from the cheapest publishable variation, which is what makes cards, price sort and price filters agree | `admin-products` `save` / `seed-store`; set prices on the **variations** |
82
+ | `stock_status` (product + variation) — from quantity + `backorders`, or rolled up from variations when the parent doesn't `manage_stock` | `admin-products` `set-stock`/`save`; order stock reduce/restore |
83
+ | `Product.average_rating` `rating_count` — from approved reviews | `admin-reviews`, `storefront-catalog` `submit-review` |
84
+ | `Product.total_sales` — first stock reduction, reversed on restore | the order transition engine |
85
+ | `count` on `ProductCategory` / `ProductRibbon` / `ProductAttributeTerm` | `admin-products`; repair with `admin-tools` `recount-terms` |
86
+ | `Order` totals (`subtotal` `discount_total` `shipping_total` `*_tax` `total`) and per-line totals | `admin-orders` `update`/`recalculate`/`apply-coupon`; `place-order` |
87
+ | `Order` status side effects — `date_paid` `date_completed` `stock_reduced` `coupon_usages_counted` `download_permissions_granted` `emails_sent` `hold_expires_at`; flag-guarded so a re-entered status never double-fires | `admin-orders` `update-status`/`bulk-status` — never a direct `status` write |
88
+ | `Order.total_refunded` | `admin-refunds` |
89
+ | `Customer.orders_count` `total_spent` `is_paying_customer` | `admin-customers` `recalculate-stats` |
90
+ | `Coupon.usage_count` `used_by` | the transition engine; repair with `admin-tools` `recount-coupon-usage` |
91
+
92
+ ## Recipes
93
+
94
+ **Enable/disable a payment gateway** — not an entity write. One call converges *every* gateway row to the set you name, validates the slugs, and is idempotent. Cards need a provider wired first ([`../install/03-data.md`](../install/03-data.md)):
95
+ ```js
96
+ await base44.functions.invoke("commerce/seed-store", { payment_methods: ["offline", "card"] });
97
+ ```
98
+
99
+ **Edit a gateway's copy or position** — ordinary config; direct CRUD, as Settings → Payments does.
100
+ ```js
101
+ await base44.entities["commerce.PaymentGateway"].update(id, { title: "Bank transfer", description: "…", order: 1 });
102
+ ```
103
+
104
+ **Read recent orders** — free-text/date search is `admin-orders` `search`; `filter` is exact-match only:
105
+ ```js
106
+ const rows = await base44.entities["commerce.Order"].filter({ status: "processing" }, "-created_date", 20);
107
+ ```
108
+
109
+ **Add an order note** — via the function: a customer note emails the buyer.
110
+ ```js
111
+ await base44.functions.invoke("commerce/admin-orders", { action: "add-note", order_id, note: "Shipped", is_customer_note: true });
112
+ ```
113
+
114
+ **Adjust a variation's stock** — via the function: it re-derives `stock_status` and sends stock alerts.
115
+ ```js
116
+ await base44.functions.invoke("commerce/admin-products", { action: "set-stock", id: product_id, variation_id, quantity: 12 });
117
+ ```
118
+
119
+ **Edit a shipping location** — direct CRUD. Rate `id`s must be **stable** (carts and orders reference the chosen rate by id), so mint one per rate and keep it. Regions and matching: [`../references/shipping-and-tax.md`](../references/shipping-and-tax.md):
120
+ ```js
121
+ const loc = await base44.entities["commerce.ShippingTaxLocation"].get(id);
122
+ await base44.entities["commerce.ShippingTaxLocation"].update(id, {
123
+ shipping_rates: [...loc.shipping_rates, { id: crypto.randomUUID(), name: "Express", cost: 25, free_over: null }],
124
+ });
125
+ ```
126
+
127
+ **Patch a settings group** — merge into `values`; replacing the object drops every other key in the group. Keys per group: [`../references/store-settings.md`](../references/store-settings.md):
128
+ ```js
129
+ const [g] = await base44.entities["commerce.StoreSettings"].filter({ group_id: "products" }, undefined, 1);
130
+ await base44.entities["commerce.StoreSettings"].update(g.id, { values: { ...g.values, auto_approve_reviews: true } });
131
+ ```
132
+
133
+ ## Where the rest lives
134
+
135
+ - A function's actions, payloads and error codes → [`api-admin.md`](./api-admin.md) · [`api-storefront.md`](./api-storefront.md)
136
+ - One entity's complete field list, types, enums, defaults → `base44/entities/commerce.<Name>.jsonc` (one read, never a directory scan)
137
+ - Seeding catalog / locations / payment methods in one call → [`api-admin.md#commerceseed-store`](./api-admin.md#commerceseed-store); worked example in [`../install/03-data.md`](../install/03-data.md)
@@ -0,0 +1,101 @@
1
+ ---
2
+ stage: install/01
3
+ read_when: "The commerce kit's files were just copied into the app, or you are installing it now."
4
+ skip_when: "The admin already mounts at /store-admin/* behind the shipped AuthGuard and / routes somewhere real."
5
+ forget_when: "The checklist at the bottom of this file passes (admin mounts, / routes somewhere real, /order-received exists)."
6
+ carry_forward:
7
+ - "Admin enforcement is three layers — AuthGuard (UI), admin-only entity RLS, requireAdmin() in every admin function. Never weaken any of them."
8
+ - "/order-received must exist as a route: every payment link returns there, and confirming is what marks an order paid."
9
+ - "Interleave: start image generation first → mount admin + build the storefront while images render → seed when the URLs are back → payments last."
10
+ - "Entities are dotted + bracket-syntax only (`base44.entities[\"commerce.X\"]`); the map is ../docs/entities.md — never scan base44/entities/."
11
+ ---
12
+
13
+ # 01 — Install
14
+
15
+ What lands in the app, how the admin gets mounted, and the order to do the rest of the work in.
16
+
17
+ **Inside the Base44 runtime, writing a resource file *is* the deploy** — entities, functions and the `commerce/StoreAdmin` agent go live the moment the files exist. Nothing to push, no build step. If `scripts/install.js` ran, everything below is already in place.
18
+
19
+ | Copied to | What it is |
20
+ |---|---|
21
+ | `base44/entities/commerce.*.jsonc` | 20 entity schemas, all admin-only RLS |
22
+ | `base44/functions/commerce/` + `shared/` + `agents/` | 16 functions, the engine, the StoreAdmin copilot |
23
+ | `src/commerce/admin/` | the finished admin app — **don't validate it, it ships tested** |
24
+ | `src/commerce/storefront/` + `utils/` | the headless hooks you build the storefront UI on ([`./02-storefront.md`](./02-storefront.md)) |
25
+ | `.agents/skills/commerce/` | these docs |
26
+
27
+ <details>
28
+ <summary>CLI path (outside the runtime only — as a Base44 agent, skip all of it)</summary>
29
+
30
+ ```bash
31
+ node examples/commerce/scripts/install.js # copies the table above
32
+ npx base44 entities push && npx base44 functions deploy && npx base44 agents push
33
+ ```
34
+ Confirm `base44/config.jsonc`'s `entitiesDir`/`functionsDir` point at those folders (the defaults do). Granting a user the `admin` role is an operator step, not yours.
35
+ </details>
36
+
37
+ **Dependencies.** Check `package.json` for `sonner`, `recharts` and `react-markdown`, and `npm i` **only** the ones actually absent — all three ship with the default Base44 template, so the normal outcome is installing nothing. Never re-install a package that is already a dependency. The kit needs no other dependency; verify the shadcn primitives listed in `src/commerce/admin/README.md` exist.
38
+
39
+ ## Work order — interleave, don't queue
40
+
41
+ Image generation is the slowest step of the install and nothing depends on it until seed time. The storefront doesn't wait on live data either — every shape you build against is documented in [`./02-storefront.md`](./02-storefront.md). So:
42
+
43
+ 1. **Start image generation first** — every product image, before anything else.
44
+ 2. **Mount the admin (below) and build the storefront** while the images render.
45
+ 3. **Seed the moment the image URLs are back** — one `commerce/seed-store` call ([`./03-data.md`](./03-data.md)); don't idle on it, pick the response (slugs) up when you need it.
46
+ 4. **Converge**: open the finished pages against the live catalog.
47
+ 5. **Payments last, if at all** — cards are off by default and nothing above depends on them ([`./03-data.md`](./03-data.md) decides it).
48
+
49
+ The only real dependency edges are *image URLs → seed payload* and *seed done → real products on the pages*. Everything else overlaps.
50
+
51
+ ## Mount the admin router
52
+
53
+ ```jsx
54
+ import AdminApp from "@/commerce/admin";
55
+ import { Navigate } from "react-router-dom";
56
+
57
+ <Route path="/store-admin/*" element={<AdminApp />} />
58
+ <Route path="/" element={<Navigate to="/store-admin" replace />} /> {/* until a storefront exists */}
59
+ <Route path="/order-received" element={<OrderReceived />} /> {/* mandatory — see below */}
60
+ ```
61
+
62
+ - **The `/*` splat is required.** The admin renders nested routes; a bare `path="/store-admin"` matches only the dashboard and every deeper link 404s. Mounting elsewhere: `<AdminApp basePath="/backoffice" />` — the prefix *without* the splat.
63
+ - **Give `/` something.** A blank Base44 app has no `/` route, so after mounting only the admin the app's own URL renders "page not found", which reads exactly like a broken install. Redirect until the storefront exists.
64
+ - **`/order-received` is mandatory**, even for a store that only ever takes offline payments. Every payment link (checkout, the admin's payment link, emails) returns there, and confirming is what marks an order paid — without the route a paying customer hits a 404 and the order stays unpaid. The page is one hook, `useOrderReturn()`, plus your markup for its states ([`./02-storefront.md`](./02-storefront.md)). A different path must be set in Settings → General → *Payment return path*.
65
+
66
+ ## Admin-role enforcement — do not weaken
67
+
68
+ The shipped `AuthGuard` requires an authenticated user whose `role === "admin"`: not signed in → "Please sign in"; signed in without the role → "Admin access required". Grant it in the Base44 dashboard or with `base44.users.inviteUser(email, "admin")`.
69
+
70
+ That guard is the **first of three layers**, and all three are load-bearing:
71
+
72
+ 1. **UI guard** — `AuthGuard` around every admin route (client-side; convenience and UX).
73
+ 2. **Entity RLS** — every commerce entity carries `"user_condition": { "role": "admin" }` on all operations, so a direct SDK read or write by a non-admin is rejected by the backend.
74
+ 3. **Function guard** — every `commerce/admin-*` function and `commerce/seed-store` calls `requireAdmin()` before touching data via the service role: **401** unauthenticated, **403** not an admin.
75
+
76
+ Bypassing the client guard therefore reaches nothing. Storefront functions are public on purpose and verify per action instead (auth session, `cart_token`, or `order_key`). The StoreAdmin agent has no entity tools and no service role for the same reason: its calls run with the chatting user's own credentials, so layer 3 still authorizes them.
77
+
78
+ Check the install at any point:
79
+
80
+ ```js
81
+ const { data } = (await base44.functions.invoke("commerce/admin-tools", { action: "status" })).data;
82
+ // → { template_version, seeded, settings_groups, counts: { "commerce.Product": n, … },
83
+ // checks: { has_payment_gateways, has_default_location } }
84
+ ```
85
+
86
+ ## Done — forget this file
87
+
88
+ - [ ] `/store-admin/*` mounted with the splat, behind the shipped `AuthGuard`; the three enforcement layers untouched.
89
+ - [ ] `/` routes somewhere real — the storefront, or the redirect above.
90
+ - [ ] `/order-received` is a route in the app's `<Routes>`.
91
+ - [ ] `sonner` / `recharts` / `react-markdown` present (installed only if they were missing).
92
+ - [ ] Image generation is already running, or the store has no product images to make.
93
+
94
+ Then continue: **[`./02-storefront.md`](./02-storefront.md) when you start building UI**, **[`./03-data.md`](./03-data.md) when you start the seed payload**. Do **not** read them now, and do not open any `references/` or `docs/` file during an install — the stage files say when one is genuinely needed, and a file read early costs its size on every later call.
95
+
96
+ Record these lines in your working notes; do not re-read this file.
97
+
98
+ - Admin enforcement is three layers — AuthGuard (UI), admin-only entity RLS, requireAdmin() in every admin function. Never weaken any of them.
99
+ - `/order-received` must exist as a route: every payment link returns there, and confirming is what marks an order paid.
100
+ - Interleave: start image generation first → mount admin + build the storefront while images render → seed when the URLs are back → payments last.
101
+ - Entities are dotted + bracket-syntax only (`base44.entities["commerce.X"]`); the map is [`../docs/entities.md`](../docs/entities.md) — never scan `base44/entities/`.