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
/storefront/productsNo authReturns paginated products for the current org. Filters and sort flow as query string.
| Field | Type | Description |
|---|---|---|
| query | string | Free-text search over name, description, SKU. |
| category | string | Slug or ID of a category to filter by. |
| brand | string | Brand name (matches the |
| minPrice | number | Lower bound on |
| maxPrice | number | Upper bound on |
| page | number | 1-based page index. Default |
| ps | number | Page size. Default |
| s | string | Sort field. Default |
| st | string |
|
// 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
/storefront/product/:idNo auth/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
/storefront/categoriesNo auth/storefront/brands/:brand?No auth/storefront/collections/:collection?No auth/storefront/attributes/:attribute?No authCategories 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.
/storefront/pricing/calculateNo auth/storefront/pricing/calculate-cartNo auth/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.