---
title: "Example: E-Commerce Catalog · RUAL Documentation"
description: "Products with variants, stock and category filtering."
canonical: https://docs.rual.nl/examples/ecommerce-catalog
language: en
---

# Example: E-Commerce Catalog

Products, variants, categories, stock, and storefront search. A complete catalog with the data model that makes it fast.

A catalog looks relational but shouldn't be modeled that way on RUAL: products carry everything their page shows. This example covers the model, the storefront, and stock handling that can't double-sell.

### The Product Document

```
{
  "_meta": {
    "cms": 1780531200118,
    "created": 1780531200,
    "entity": 1,
    "expiry": -1,
    "guid": "c45797061f2c15a75361042244385e96419d230ab804d7fa08cd52981fd78192",
    "removed": 0,
    "ums": 1782166695204,
    "update_hash": "f1a9c2d47b6035e8419dc0a7532be96481cd30fa",
    "updated": 1782166695
  },
  "category": "apparel",
  "image": "/static/products/tshirt-black.webp",
  "name": "essential cotton t-shirt",
  "price": 2490,
  "sku": "TSHIRT-BLK-M",
  "status": "active",
  "variants": [
    {
      "size": "m",
      "sku": "TSHIRT-BLK-M",
      "stock": 14
    },
    {
      "size": "l",
      "sku": "TSHIRT-BLK-L",
      "stock": 3
    }
  ]
}
```

- Variants live inside the product. A product page reads one document. Never joins (see [catalog model](https://docs.rual.nl/blueprints/storage-examples#product-catalog)).

- Price in cents with `default_divide_by = 100`: no float rounding bugs.

- Everything stored lowercase so case-sensitive search just works.

### The Storefront

- **Category page**: term filter on `category` + `status = active`, sorted by name, paginated with `limit`/`offset`.

- **Search box**: [`query_bool_simple_query_string_field`](https://docs.rual.nl/block-types/query/query_bool_simple_query_string_field) on `name` wrapped in [`query_bool_must`](https://docs.rual.nl/block-types/query/query_bool_must), with the active-filter in [`query_bool_filter`](https://docs.rual.nl/block-types/query/query_bool_filter) via `query_and`.

- **Product page**: fetch by `sku` with [`function_search_single_result`](https://docs.rual.nl/block-types/storage/function_search_single_result); render variants from the embedded array, disabling out-of-stock sizes in the UI.

- **Autocomplete**: prefix/wildcard query on `name` behind a small API. The pattern in [search optimization](https://docs.rual.nl/blueprints/storage-examples#search-optimization).

### Stock Without Double-Selling

- Checkout decrements stock with [`mutations_increment_by_field`](https://docs.rual.nl/block-types/mutations/mutations_increment_by_field) (`-quantity`) inside `function_update_document`: mutation transactions process sequentially, so two checkouts can't overwrite each other.

- Guard the sale: only decrement when `stock >= quantity`; on the failure path, hold the order as `backorder` instead of rejecting the customer.

- Restock is the same increment with a positive value. One block, both directions.

### Admin Side

- Product management pages scoped `*loggedin` + `catalog_manager` custom scope. See [User Authentication](https://docs.rual.nl/tutorials/user-authentication).

- Price/name changes go through [`function_update_document_mutations`](https://docs.rual.nl/block-types/storage/function_update_document_mutations); images via the asset pipeline in [File Upload & Processing](https://docs.rual.nl/tutorials/file-upload-processing).

- Discontinuing is a status change, not a delete: old orders keep their embedded snapshots intact.

### Next Steps
