# Build e-commerce storefronts

A digital commerce team runs the storefront of an online store on a commerce platform it operates, such as Magento or WooCommerce. Catalog and product pages are the same for every visitor and must stay fast during traffic peaks, while prices and stock change during the day. The cart, the checkout, and the pages of a signed-in visitor are different for every visitor and must never come from a shared copy. This page configures an application in front of the store that caches the catalog, sends cart, checkout, and session traffic to the store, and purges a product page when it changes. The result is measured by time to first byte on catalog and product pages, the time from a price or stock change to a live page, and the share of catalog requests that never reach the commerce platform.

This use case does not cover protecting login and checkout against bots. For that, refer to [Block account takeover on login and checkout flows](/en/documentation/use-cases/secure-applications-and-networks/block-account-takeover-on-login-and-checkout-flows/).

## Prerequisites

- An application that serves the store through a connector and a workload. To create them, refer to [Applications quickstart](/en/documentation/platform/applications/quickstart/).
- Application Accelerator on that application, which the **Bypass Cache** and **Forward Cookies** behaviors require. To turn it on, refer to [Turn on Application Accelerator](/en/documentation/guides/application-performance/cache-and-purge/cache-settings/#turn-on-application-accelerator).
- A personal token, for the API and the purge calls. To create one, refer to [Personal tokens](/en/documentation/guides/platform/account-and-billing/personal-tokens/).
- The paths and the session cookie of your store. This page uses `/products/` and `/category/` for the catalog, `/cart`, `/checkout`, and `/account` for per-visitor pages, `session_id` for the cookie the store sets when a visitor starts a session, and `www.example.com` for the domain. Replace each value with your store's in every step.

---

## Required products

| The storefront needs                                                 | Which means                                                                                    | Product                 | Documented in                                                                                                                               |
| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Catalog and product pages that answer from cache                     | A cache setting applied by a rule matched on the catalog paths, for visitors without a session | Cache                   | [Cache settings](/en/documentation/platform/applications/cache/cache-settings/)                                                             |
| Cart, checkout, and session pages that never come from a shared copy | A rule that bypasses the cache by path and by session cookie                                   | Application Accelerator | [Bypass the cache for a path](/en/documentation/guides/application-performance/cache-and-purge/cache-settings/#bypass-the-cache-for-a-path) |
| Product pages that refresh when the catalog changes                  | A purge by URL that the commerce platform sends when a product changes                         | Cache                   | [Purge pages when the origin publishes a change](/en/documentation/guides/application-performance/cache-and-purge/purge-on-publish/)        |
| Product images at the size and format each page asks for             | Image Processor on the application, applied by a rule on image paths                           | Image Processor         | [Image Processor quickstart](/en/documentation/platform/applications/image-processor/quickstart/)                                           |

---

## Reference architecture

This page builds the *Origin-hosted commerce platform storefront*: an application in front of a store that keeps rendering every page.

```mermaid
%%{init: {"layout": "dagre", "themeVariables": {"fontSize": "13px"}, "flowchart": {"nodeSpacing": 12, "rankSpacing": 12, "padding": 6, "wrappingWidth": 70, "minNodeWidth": 40, "useMaxWidth": true}}}%%
flowchart TD
  Visitor["Visitor"] -->|"HTTPS request"| App["application"]
  App -->|"Rules Engine: catalog path, no session"| Cache["Cache"]
  App -->|"Rules Engine: cart, checkout, or session cookie"| Connector["connector"]
  App -->|"image path"| IP["Image Processor"]
  Cache -->|"miss"| Connector
  IP --> Connector
  Connector --> Store["commerce platform"]
  Store -->|"catalog update"| Purge["Real-Time Purge"]
  Purge -->|"invalidates product pages"| Cache
```

Read the diagram from the application outward. Every request passes through its rules, which split it into three paths: catalog pages that Cache can answer, per-visitor pages that must reach the store, and images that Image Processor transforms. All three paths end at the same connector, because the commerce platform remains the only origin. The loop from the store back to Cache is the purge, which keeps cached pages in step with the catalog.

### Dataflow

1. A visitor's request reaches the workload on the store's domain, which hands it to the application, and Rules Engine reads its path and its `session_id` cookie in the Request Phase.
2. A catalog or product page requested without a session cookie is served from Cache. On a miss, the application fetches it from the store through the connector and caches it.
3. A cart, checkout, or account request, or any request that carries the session cookie, bypasses the cache and goes to the store through the connector.
4. An image request goes through Image Processor, which resizes or converts the original fetched from the store, and Cache keeps each variation.
5. When a product changes, the commerce platform calls the Real-Time Purge API, which removes the affected product pages from Cache before their TTL ends.
6. The next request for a purged page reaches the store, and the new version is cached.

### Components

- **application**: the Platform Resource in front of the store. It holds the cache settings and the rules that decide, request by request, whether a page is cached or bypassed.
- **Rules Engine**: the Feature of the application that matches paths and cookies. The cache bypass for a signed-in visitor depends on a cookie condition, because the same product path is shared for an anonymous visitor and private for a signed-in one.
- **connector**: the Platform Resource that reaches the store. Every path, cached or not, ends at it, because the store renders every page.
- **Cache**: stores catalog and product pages, so repeated requests for them never reach the store.
- **Real-Time Purge**: the Platform Resource that removes a changed product page from Cache before its TTL ends, so a price or stock change goes live without waiting for the page to expire.
- **Image Processor**: resizes and converts product images on request, so the store keeps one original per image.
- **commerce platform**: the integration that is the origin and the content source. It renders every page, owns the cart and the checkout, and sends the purge when the catalog changes.

### Other designs for this use case

- *Statically generated headless commerce storefront*: for catalogs that change a few times a day, where a site generator pulls products from the commerce platform's API at build time. Catalog pages are prebuilt in Object Storage and served through Cache, so price and stock freshness depend on rebuilds, and only cart and checkout reach the commerce API.
- *Server-rendered headless commerce storefront*: for catalogs with frequent price and stock changes or prices per region, where functions render pages with a framework such as Next.js and call the commerce API. Pages render on request, so the commerce API is in the request and failure flows, and freshness is a caching decision instead of a rebuild.

---

## Configure the catalog cache

The catalog cache is a cache setting and the rule that applies it. The rule matches the catalog paths only for visitors without a session, so a signed-in visitor never receives a shared copy.

The cache setting uses two values. **Max Age** is `600` seconds: the purge configured below refreshes a changed page at once, so **Max Age** only bounds how long a page stays stale when a purge is missed. The browser cache is overridden to `0` seconds, because a copy in the visitor's browser cannot be purged, and a stale price would stay there until it expires.

**Console**

To create the cache setting:

1. **Open the Cache Settings tab**

   Access [Azion Console](https://console.azion.com/) > **Applications** > **your application**, then go to the **Cache Settings** tab.

2. **Select + Cache**

3. **Name the cache setting**

   In **Name**, enter `storefront-catalog`.

4. **Set the browser cache**

   Under **Browser Cache**, select *Override cache settings* and set the maximum age to `0`.

5. **Set Max Age**

   Under **Cache**, select *Override cache behavior* and set **Max Age** to `600`.

6. **Select Save**

The `storefront-catalog` setting appears in the **Cache Settings** tab. To create the rule that applies it:

1. **Go to the Rules Engine tab**

2. **Select + Rule**

3. **Name the rule**

   Enter `storefront - catalog cache`.

4. **Select Request Phase**

5. **Match the catalog paths**

   In the **Criteria** section, set the first criterion to `${uri}` *starts with* `/products/`. Add a second criterion joined by **Or**: `${uri}` *starts with* `/category/`.

6. **Exclude visitors with a session**

   Add a second criteria group with one criterion: `${cookie_session_id}` *does not exist*.

7. **In the Behaviors section, select Set Cache Policy**

8. **Select the storefront-catalog cache setting**

9. **Select Save**

**API**

To create the cache setting, send its body to the application's cache settings:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/cache_settings \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "storefront-catalog",
  "browser_cache": { "behavior": "override", "max_age": 0 },
  "modules": { "cache": { "behavior": "override", "max_age": 600 } }
}'
```

The API answers `201` with the new setting. Keep its `id` for the rule:

```json
{"state":"executed","data":{"id":<cache-setting-id>,"name":"storefront-catalog",...}}
```

To create the rule, send two criteria groups. Groups join with `and`, so a request matches only when its path is a catalog path and it carries no session cookie:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/request_rules \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "storefront - catalog cache",
  "active": true,
  "criteria": [
    [
      { "variable": "${uri}", "conditional": "if", "operator": "starts_with", "argument": "/products/" },
      { "variable": "${uri}", "conditional": "or", "operator": "starts_with", "argument": "/category/" }
    ],
    [
      { "variable": "${cookie_session_id}", "conditional": "if", "operator": "does_not_exist" }
    ]
  ],
  "behaviors": [{ "type": "set_cache_policy", "attributes": { "value": <cache-setting-id> } }]
}'
```

The API answers `202` with a `state` of `pending` and the rule as it was stored.

Catalog and product pages requested without a session cookie are cached for 600 seconds, and browsers revalidate them on every visit. A new rule takes a few minutes to propagate.

---

## Configure the bypass for cart, checkout, and sessions

The bypass rule sends every per-visitor request to the store. It matches the cart, checkout, and account paths, and any request that carries the session cookie, so a visitor who added an item to the cart also bypasses the cache on catalog pages. The rule carries **Forward Cookies** as well, so the session cookie the store sets reaches the visitor. Nothing this rule matches is cached, so no visitor can receive another visitor's cookie.

```mermaid
%%{init: {"layout": "dagre", "themeVariables": {"fontSize": "13px"}, "flowchart": {"nodeSpacing": 12, "rankSpacing": 12, "padding": 6, "wrappingWidth": 70, "minNodeWidth": 40, "useMaxWidth": true}}}%%
flowchart TD
  Req["Request"] --> Cookie{"session_id cookie?"}
  Cookie -->|"yes"| Bypass["Bypass Cache and Forward Cookies"]
  Cookie -->|"no"| Path{"Path"}
  Path -->|"/cart, /checkout, /account"| Bypass
  Path -->|"/products/, /category/"| Cached["storefront-catalog cache setting"]
  Path -->|"any other path"| Default["the application's other rules"]
  Bypass --> Store["commerce platform"]
  Cached -->|"miss"| Store
```

1. A request that carries the `session_id` cookie bypasses the cache, whatever its path.
2. A request without the cookie bypasses the cache on `/cart`, `/checkout`, and `/account`.
3. A request without the cookie on `/products/` or `/category/` takes the `storefront-catalog` cache setting.
4. Any other path is left to the application's other rules.

The rule joins two procedures of [Configure cache policies for an application](/en/documentation/guides/application-performance/cache-and-purge/cache-settings/), [Bypass the cache for a path](/en/documentation/guides/application-performance/cache-and-purge/cache-settings/#bypass-the-cache-for-a-path) and [Forward cookies from the origin to the user](/en/documentation/guides/application-performance/cache-and-purge/cache-settings/#forward-cookies-from-the-origin-to-the-user), in one rule with the store's four criteria joined by **Or**. The guide's denylist cache setting is left out, because nothing this rule matches is cached:

**Console**

To create the bypass rule:

1. **Go to the Rules Engine tab**

   Access [Azion Console](https://console.azion.com/) > **Applications** > **your application**, then go to the **Rules Engine** tab.

2. **Select + Rule**

3. **Name the rule**

   Enter `storefront - bypass per-visitor pages`.

4. **Select Request Phase**

5. **Match the per-visitor paths**

   In the **Criteria** section, set the first criterion to `${uri}` *starts with* `/cart`. Add two criteria joined by **Or**: `${uri}` *starts with* `/checkout`, and `${uri}` *starts with* `/account`.

6. **Match the session cookie**

   Add a fourth criterion joined by **Or**: `${cookie_session_id}` *exists*.

7. **In the Behaviors section, select Bypass Cache**

8. **Add the Forward Cookies behavior**

9. **Select Save**

**API**

To create the bypass rule, send its four criteria in one group joined by `or`, and both behaviors:

```bash
curl --request POST \
  --url https://api.azion.com/v4/workspace/applications/<application-id>/request_rules \
  --header 'Authorization: Token <personal-token>' \
  --header 'Content-Type: application/json' \
  --data '{
  "name": "storefront - bypass per-visitor pages",
  "active": true,
  "criteria": [
    [
      { "variable": "${uri}", "conditional": "if", "operator": "starts_with", "argument": "/cart" },
      { "variable": "${uri}", "conditional": "or", "operator": "starts_with", "argument": "/checkout" },
      { "variable": "${uri}", "conditional": "or", "operator": "starts_with", "argument": "/account" },
      { "variable": "${cookie_session_id}", "conditional": "or", "operator": "exists" }
    ]
  ],
  "behaviors": [{ "type": "bypass_cache" }, { "type": "forward_cookies" }]
}'
```

The API answers `202` with a `state` of `pending` and the rule as it was stored.

Cart, checkout, and account requests, and every request with a session, reach the store, and Azion stores none of their responses. A new rule takes a few minutes to propagate.

---

## Configure purge on catalog changes

A changed product page is refreshed by a purge that the commerce platform sends when the product is saved. The call goes in the code the platform runs when a product is saved, and it authenticates with the personal token.

```mermaid
%%{init: {"layout": "dagre", "themeVariables": {"fontSize": "13px"}, "flowchart": {"nodeSpacing": 12, "rankSpacing": 12, "padding": 6, "wrappingWidth": 70, "minNodeWidth": 40, "useMaxWidth": true}}}%%
sequenceDiagram
  participant Store as commerce platform
  participant Purge as Purge API
  participant Cache as Cache
  participant Visitor as Visitor
  Note over Store: A product is saved
  Store->>Purge: URL purge
  Purge-->>Store: 201 executed
  Purge->>Cache: Removes the pages
  Visitor->>Cache: Product page
  Cache->>Store: Miss, fetch
  Store-->>Visitor: New price
```

1. An admin saves a product, and the platform runs its save code.
2. The save code sends a URL purge for the product page and its category page, and a wildcard purge for the images when they changed.
3. Azion removes both pages from the cache once the purge propagates.
4. The next request for each page reaches the store, and the new version is cached.

The save code sends the purges that [Purge pages when the origin publishes a change](/en/documentation/guides/application-performance/cache-and-purge/purge-on-publish/) describes, with the store's values:

- **On every save**, a URL purge for the product page and the category pages that list it. For the example product, the body of `POST /v4/workspace/purge/url` is:

  ```json
  {"items":["https://www.example.com/products/blue-shirt","https://www.example.com/category/shirts"],"layer":"cache"}
  ```

- **Only when the save changes the product's images**, a wildcard purge for every size and format of them. Azion accepts 2,000 wildcard purge requests in a 24-hour interval, so a wildcard on every save of a large catalog can reach that bound. The body of `POST /v4/workspace/purge/wildcard` is:

  ```json
  {"items":["www.example.com/media/blue-shirt*"]}
  ```

Each call answers `201` with `state` set to `executed`. The product page, its category page, and any changed images leave the cache, and the next request for each one fetches the new version from the store. To confirm a purge completed, find it in the purge history, as the guide shows.

---

## Verify the setup

Each check sends a request with the `Pragma: azion-debug-cache` header, which makes the response carry the `x-cache` header. For how to read it, refer to [Check the cache status of a response](/en/documentation/guides/application-performance/cache-and-purge/check-page-cache-time/).

- **Catalog pages answer from cache.** Request a product page twice without a cookie:

  ```bash
  curl -sI -H "Pragma: azion-debug-cache" https://www.example.com/products/blue-shirt
  ```

  The second response carries `x-cache: HIT`. The first can carry `MISS`, while Azion fetches the page from the store.

- **The cart never comes from cache.** Request the cart:

  ```bash
  curl -sI -H "Pragma: azion-debug-cache" https://www.example.com/cart
  ```

  The response carries `x-cache: BYPASS`.

- **A visitor with a session never gets a shared catalog page.** Request a product page with the session cookie:

  ```bash
  curl -sI -H "Pragma: azion-debug-cache" -H "Cookie: session_id=test" https://www.example.com/products/blue-shirt
  ```

  The response carries `x-cache: BYPASS`.

- **A product change reaches the page.** Send the purge for `/products/blue-shirt`, wait for it to appear in the purge history, and request the page again. The response carries `x-cache: MISS`, and the request after it carries `HIT`.

- **Product images are processed.** Request an image with an `ims` query string, such as `https://www.example.com/media/blue-shirt.jpg?ims=400x`. The response carries `x-ims: Enabled`.

A rule that seems to have no effect may still be propagating. When it persists after a few minutes, turn on [Debug Rules](/en/documentation/platform/applications/main-settings/#debug-rules) to see which rules ran on the request.

---

## Measuring results

| Metric                                               | Where to read it                                                                                                                                                                                               | What working looks like                                                                  |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Share of catalog requests that never reach the store | **Requests Offloaded** in Real-Time Metrics, filtered to the store's domain. Refer to [Measure cache offload for a domain](/en/documentation/guides/platform/observability/measure-cache-offload/)             | Rises after the catalog rule propagates, and holds during traffic peaks                  |
| Time to first byte on catalog and product pages      | The `ttfb` field of Edge Pulse measurements, taken in the visitors' browsers. Refer to [Edge Pulse quickstart](/en/documentation/platform/edge-pulse/quickstart/)                                              | Lower on catalog pages than on cart and checkout pages, in every region                  |
| Time from a price or stock change to a live page     | The purge history of **Real-Time Purge** in Azion Console, which lists each purge when it is complete. Refer to [Real-Time Purge](/en/documentation/platform/applications/cache/real-time-purge/#confirmation) | Each purge completes, and no product page waits for its 600-second **Max Age** to expire |

---

## Best practices

- **Exclude sessions with a criterion, not with cookie variation.** Varying the catalog cache key by `session_id` stores one copy per visitor, because the cookie is unique per visitor. The `does not exist` criterion keeps a single shared copy for every visitor without a session. For cookie variation and its cost, refer to [Applications best practices](/en/documentation/platform/applications/best-practices/).
- **Never put Forward Cookies on a rule that caches.** On a cached response, **Forward Cookies** can hand one visitor the `Set-Cookie` of another visitor's session. On this page it sits only on the bypass rule, where nothing is cached.
- **Purge on change instead of shortening Max Age.** A short **Max Age** sends more catalog requests to the store and still leaves a stale window. The purge refreshes the page when it changes, and **Max Age** stays a safety bound for a missed purge.
- **Use Bypass Cache for the cart, not a Max Age of 0.** A **Max Age** of `0` merges simultaneous requests for one path into one origin request, and two visitors who load their carts at the same moment need different answers. For the difference between the two, refer to [Cache variation](/en/documentation/platform/applications/application-accelerator/cache-variation/#bypass-cache-and-a-ttl-of-0).

---

## Guides in this use case

- [Purge pages when the origin publishes a change](/en/documentation/guides/application-performance/cache-and-purge/purge-on-publish.md): Sends the purge calls the store makes on each product save, and confirms each one completed.
- [Configure cache policies for an application](/en/documentation/guides/application-performance/cache-and-purge/cache-settings.md): Bypasses the cache and forwards cookies on a path, the two behaviors of the cart, checkout, and session rule.
