Categories and products
Building the catalogue — category rules, every product field and what it controls, image paths that actually validate, inventory behaviour, soft deletes, reviews and wishlists.
The catalogue is two tables and a handful of rules. Products belong to exactly one category, carry exactly one price in exactly one currency, and are visible or not. There are no variants, no option sets and no per-product overrides for tax or shipping — those are store-wide settings.
Build the categories first. A product cannot be created without one.
Categories
Admin → E-commerce → Catalog → Categories (/admin/ecommerce/category).
| Field | Rules |
|---|---|
name |
Required, up to 191 characters |
slug |
Required, up to 191 characters. Leave it blank on create and it is generated from the name — lowercased, non-alphanumerics collapsed to dashes — and de-duplicated with a numeric suffix if it already exists |
description |
Required, and capped at 191 characters. This is a short column, not a rich text field |
image |
Optional, but must be a platform-hosted path |
status |
Active/inactive toggle |
/ecommerce/category/<slug> requires status: true. An inactive category
answers 404 and its product listing disappears with it. The products stay
active and still show under /ecommerce/product — which is exactly the
half-visible state that makes a catalogue look broken.
Active categories are also pulled into the storefront navigation. The layout
fetches /api/ecommerce/category on mount and renders every active row as a
child item under Categories, so publishing a category changes the menu with
no configuration.
Deleting a category
Deleting is a soft delete — the row gets a deletedAt and can be restored from
the same screen. A permanent delete is a different matter: the product
foreign key cascades, so removing a category for good removes its products with
it, and product removal cascades further into reviews, discounts, wishlist
items and order items.
Deactivate categories you have finished with. Reserve permanent deletion for rows that never had a sale.
Products
Admin → E-commerce → Catalog → Products (/admin/ecommerce/product).
| Field | Type | Notes |
|---|---|---|
name |
text | Required |
slug |
text | Unique across the whole catalogue. Generated from the name if you leave it blank |
description |
long text | Required. The full product page body |
shortDescription |
text | Optional, 191 characters. Shown in listings |
type |
PHYSICAL | DOWNLOADABLE |
Required. Decides everything downstream |
price |
number | Required, cannot be negative |
currency |
text | Required, defaults to USD. Must be a currency enabled for the chosen wallet type |
walletType |
FIAT | SPOT | ECO |
Required, defaults to SPOT. ECO appears only when the Ecosystem addon is installed |
categoryId |
select | Required |
inventoryQuantity |
integer | Required, cannot be negative. Ignored for downloadable products |
image |
image | Optional. Must be a platform-hosted path |
status |
toggle | Active/inactive |
Price, currency and wallet type travel together
The buyer must hold a wallet matching the product's wallet type and currency
pair. Checkout looks for exactly that wallet, and if it is missing or short
the customer gets Insufficient balance in <CURRENCY> wallet — the same
message either way, so "I have money" support tickets are usually a missing
wallet, not a missing balance.
The currency dropdown is driven by the wallet type you pick and repopulates when you change it. An empty list means core has no enabled currencies for that wallet type.
Order rows record subtotal, discount, shippingCost, tax, total,
currency and walletType at the moment of sale, and refunds are computed
from the original transaction — so historic orders are unaffected by later
edits. But a customer part-way through checkout is quoting the old figures.
Deactivate, edit, reactivate.
Images must be platform-hosted
Both the product and category image fields are validated against
^/(uploads|img)/.*$. An external URL is rejected outright with
"Image must be a valid URL", which is a confusing message for a perfectly
valid URL. Upload the file through the form; it lands under
/uploads/... and validates.
One image per product. There is no gallery.
Inventory
Stock is a plain integer on the product row, and it moves in exactly three places:
- Checkout decrements it with a conditional update
(
WHERE inventoryQuantity >= quantity). If a concurrent order took the last unit, the update matches nothing and the whole checkout rolls back withProduct inventory changed during checkout. Overselling is not possible. - Cancel and reject restore it, for physical items only, as part of the same transaction that refunds the buyer.
- You edit it on the product form.
Downloadable products never touch inventory. Set it to zero and they still sell.
The storefront listing filters on status: true only — there is no inventory
filter and no setting that adds one. A product at zero stock keeps its page,
keeps its "add to cart" button, and fails at checkout with
Insufficient inventory. If you do not want that, deactivate the product when
it sells out.
Status and deletion
status: false removes a product from every storefront listing, from search
and from category pages, and makes checkout refuse it with
Product is not available. It stays visible in the admin panel and on existing
orders.
Deletes are soft by default and restorable from the products table. A permanent delete cascades into reviews, discounts, wishlist items and order items — which is to say it can quietly rewrite the history of an order somebody paid for. Deactivate instead.
The order row survives, but its order item does not. The order still shows in the buyer's history and in your revenue figures with nothing in it, and a downloadable purchase loses the licence key or file attached to that item.
Reviews
Reviews are attached to products and gated on a real purchase.
- A customer may only review a product they hold in a
COMPLETEDorder. Anyone else getsYou have not purchased this product. A pending physical order does not qualify — the buyer can review it once you fulfil it. - One review per customer per product. Submitting again updates the existing one rather than adding a second.
ratingis required;commentis optional.- Submissions are rate-limited to keep a compromised account from flooding a product page.
Moderate at Admin → E-commerce → Catalog → Reviews
(/admin/ecommerce/review). Each review has a status boolean; only
status: true reviews are counted into the ratings shown on the storefront, so
switching one off hides it without destroying the customer's text.
Reviews arrive enabled. If you want approval-before-publication, you are moderating after the fact — there is no pre-moderation queue.
Wishlists
Every customer gets one wishlist row, holding many items. Customers manage it
at /ecommerce/wishlist; adding and removing are one endpoint each.
Admin → E-commerce → Catalog → Wishlist (/admin/ecommerce/wishlist) is a
reporting surface — it tells you which products people save and never buy,
which is the most honest signal in the store about a price being wrong.
Wishlists have no effect on stock, pricing or checkout. Deleting a product removes it from every wishlist.
The storefront your catalogue produces
| Page | What it lists |
|---|---|
/ecommerce |
Landing page: store stats, featured products, best sellers, new arrivals, top rated, active deals, categories with counts, recent reviews |
/ecommerce/product |
Every active product, paginated by ecommerceProductsPerPage (default 12) |
/ecommerce/product/<slug> |
Full product page with reviews |
/ecommerce/category |
Every active category |
/ecommerce/category/<slug> |
Active products in that category |
The landing page is assembled by the backend from live catalogue data — you do not configure it. Separately, core's page builder ships ten E-commerce section templates (product grid hero, category showcase, bestsellers grid, sale banner and others) for your main site landing page. Those are static layouts you edit by hand; they do not read the catalogue.
Next: Orders and fulfilment.