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.

5 min readUpdated 3 August 2026products, categories, inventory, reviews, wishlist

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 with Product 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 COMPLETED order. Anyone else gets You 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.
  • rating is required; comment is 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.