API (api-v2)
Catalogue modules
Shops, categories, products, search, custom fields, modifiers, media and storefront sections. Route guards are shown in brackets. Every route is POST.
Guard legend
perm = requirePermission · wide = requireShopWidePermission · at = requirePermissionAt · owner = ownerOnly · oScoped = ownerScoped (closed to staff today). See Guards.
Shop
Tables: shop, shop_localization
| Route | Guard |
|---|---|
/merchant/shop/v1/get-shops, get-shop-localization, create-shop, get-user-shops, delete-shop-images | oScoped |
update-shop, delete-shop | owner |
- Fields: title, description, logo, cover, contact email and phone, language, currency, country, weight unit,
flatShippingRate,tax_rate,taxes_included. create-shoptakes no fields. In one transaction it creates the shop, a first location ("Your Location"), and the shop's ledger.- Shops are soft-deleted.
Category
Tables: category, category_localization, shop_category
| Route | Guard |
|---|---|
/merchant/auth/v1/shop-categories | perm products.view |
get-categories, get-category-localization | oScoped |
create-category, update-category, delete-category | wide categories.write |
A localized tree. This is the reference module to copy.
Product & tags
Tables:
product,product_localization,product_category,product_image,product_option*,product_variant,product_metafieldtag,tag_localization,product_tag
| Route | Guard |
|---|---|
get-shop-products, get-shop-tags | perm products.view |
get-products, get-product-localization | oScoped |
set-product | wide products.write |
delete-product | wide products.delete |
set-product is a desired-state upsert. It covers options, variants, inventory, tags, custom field values, images and categories. Anything omitted is deleted, so clients must always send the whole product.
Variant fields: price, compare-at, SKU, barcode, weight, taxable, trackQuantity, continue-selling-when-out-of-stock, physical, country of origin, preorder flag and available-on date.
Multiple categories: product_category is many-to-many, with position 0 as the primary category. product.category_id is kept as a denormalised copy of the primary.
Search
Tables: search_document, search_attribute, search_product_stock. This projection can be rebuilt.
| Route | Guard |
|---|---|
/merchant/shop/v1/search-products, search-product-facet-values, search-product-filters, search-product-filter-options, get-product-filters, search (global: products, orders, categories, sections) | perm products.view |
SEARCH_BACKEND=postgresis the default. Algolia is kept as a rollback, and writers dual-write to both.- Stock tabs (out, low, in stock) come from
search_product_stock, whichsearch.stock.changedkeeps up to date. - Filterable custom fields become facets.
Custom fields ("entities")
Tables: entity_type, entity, field, field_value (+ localizations)
| Route | Guard |
|---|---|
get-category-fields, get-entity-types, get-entities, get-entity | perm products.view |
set-entity-type, delete-entity-type, set-entity, delete-entity, set-field, delete-field | wide products.write |
- Fields are defined on a category and inherited by every descendant.
- Entity types are reference lists (contributors, publishers, series, bindings).
- Field values are exposed on the storefront product detail page.
Books aren't special-cased
There's no book code path. Books are a Books category tree with about 28 inherited fields (ISBN-13, copy type, condition, contributors…). Café items are ordinary products with modifiers and till sections.
Modifiers
Tables:
modifier_group: selection min/maxmodifier_option: price delta in USD, direct cost, ticket label, default, activeproduct_modifier_grouporder_line_item_modifier: a snapshot taken at sale time
| Route | Guard |
|---|---|
get-modifier-groups, get-product-modifier-groups | perm products.view |
create/update/delete-modifier-group, create/update/delete-modifier-option, set-product-modifier-groups | wide products.write |
/pos/v1/get-modifier-groups | pos.operate |
At create-order the server validates every selection:
- the option belongs to the shop and is active
- its group is assigned to the product
- each group's min/max is satisfied
Deltas are folded into the unit price, and each line can carry a note.
Not done yet (.scratch/cafe-modifiers/phases.md): Phase 0 (client pricing decisions) and Phase 4 (barista ticket / kitchen display).
Media
Table: image
| Route | Guard |
|---|---|
create-media-upload, register-media | requireAnyPermission over the catalogue write codes |
- Uploads are signed for Cloudinary: the server mints the
public_idand signature, the browser uploads directly, and the secret never leaves the server. - Size limits come from
MEDIA_MAX_IMAGE_BYTESandMEDIA_MAX_VIDEO_BYTES.
Storefront sections
Tables: section, section_item (an item is a product, a category or another section), section_image, shop_sections
| Route | Guard |
|---|---|
get-sections, get-section-localization, update-section | oScoped |
/merchant/auth/v1/shop-sections | perm products.view |
get-section-item-types, get-section-display-types | authenticated |
create-section, delete-section, update-section-order | wide sections.write |
These are distinct from till sections; see POS & shifts.