Documentation

Products and variants

Product catalog endpoints, variant model, categories, and tiered pricing.

Every storefront UI starts with the product catalog. Read endpoints are public — drop them straight into a server component. Writes go through the generic /data/product repository CRUD with a User principal.

List products

GET/storefront/productsNo auth

Returns paginated products for the current org. Filters and sort flow as query string.

FieldTypeDescription
querystring

Free-text search over name, description, SKU.

categorystring

Slug or ID of a category to filter by.

brandstring

Brand name (matches the brand field on the product).

minPricenumber

Lower bound on price.

maxPricenumber

Upper bound on price.

pagenumber

1-based page index. Default 1.

psnumber

Page size. Default 50, max 200.

sstring

Sort field. Default modifydate.

ststring

asc or desc. Default desc.

// base-app/src/lib/storefront-api.ts (excerpt)
import { storefrontAPI } from '@/lib/storefront-api';

const products = await storefrontAPI.getProducts({
  category: 'shoes',
  minPrice: 50,
  page: 1,
  limit: 24,
  sortBy: 'price',
  sortOrder: 'asc',
});

Get a single product

GET/storefront/product/:idNo auth
GET/storefront/product/:id/relatedNo auth

:id accepts the product SK or slug. The response is a BaseModel<Product> — read your fields off record.data.

{
  "pk": "my-org|product",
  "sk": "my-org|product|sneaker-air-1",
  "datatype": "product",
  "version": 3,
  "state": "active",
  "data": {
    "name": "Air Sneaker",
    "slug": "sneaker-air-1",
    "description": "Lightweight running shoe.",
    "sku": "SNK-AIR-1",
    "price": 89.99,
    "currency": "USD",
    "brand": "Nimbus",
    "category": "shoes",
    "images": ["https://cdn.../sneaker-1.jpg"],
    "stock": 142,
    "attributes": { "weight": "210g", "material": "knit" },
    "variations": [
      {
        "sku": "SNK-AIR-1-9-BLK",
        "options": { "size": "9", "color": "black" },
        "price": 89.99,
        "stock": 12,
        "image": "https://cdn.../sneaker-1-black.jpg"
      },
      {
        "sku": "SNK-AIR-1-10-BLK",
        "options": { "size": "10", "color": "black" },
        "price": 89.99,
        "stock": 7
      }
    ]
  }
}

The shape is verified against base-app/src/lib/storefront-api.ts — keep data.price, data.images, data.stock, and data.variations straight.

Variants

A variant is a row in data.variations. Variants share name, description, and category with the parent and override SKU, options, price, image, and stock. There is no separate /storefront/variant endpoint — work with the product and pick the variation by sku or by matching the options object.

Variation options are user-defined. The convention in base-app is { size, color, material }, but you can store any keys. The product-page UI maps the union of keys across variations to selectors.

Categories, brands, and collections

GET/storefront/categoriesNo auth
GET/storefront/brands/:brand?No auth
GET/storefront/collections/:collection?No auth
GET/storefront/attributes/:attribute?No auth

Categories return as a flat list with optional parent references. Brands and attributes return name + count. Collections are curated product groupings (think "Summer 2025") and accept the same paging params as /products.

Tiered pricing

Products carry a base price, but the real number a customer sees comes from the pricing engine — group discounts, quantity tiers, and automatic promotions all apply server-side.

POST/storefront/pricing/calculateNo auth
POST/storefront/pricing/calculate-cartNo auth
GET/storefront/pricing/product/:skuNo auth
// Compute the unit price for a given quantity
const calc = await storefrontAPI.calculatePrice('SNK-AIR-1-9-BLK', 3);
// { originalPrice: 89.99, finalPrice: 80.99, discountPercent: 10, tier: { minQty: 3, ... } }

Pricing accepts a customer context (customerId, customerGroups, tier) and returns the tier-aware finalPrice. getProductPricing returns the tier table itself — useful when you want to render "buy 3 for $80, buy 10 for $70" on the product page. See base-app/src/lib/storefront-api.ts:553-625 for the canonical wrapper.

Writes

To create or update a product, use the generic data endpoints with a User principal that carries the ContentAdmin role:

curl -X POST https://appengine.appmint.io/data/product \
  -H "orgid: my-org" -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "name": "Air Sneaker", "sku": "SNK-AIR-1", "price": 89.99 } }'

You can wire the same endpoint into a CSV importer or a build pipeline. The Product model schema is the source of truth for required fields.