Rebasely Beta: data may be purged

examples/storefront Public

Markdown · 2.1 KiB · at 80dbc76

id: catalog
type: concept
owner: justin
component: storefront-api
describes:
  - src/catalog.ts
  - migrations/0001_products.sql
reviewed: 2026-10-13

Catalog

The catalog is the products table in D1 ([[0001-d1-for-shop-data]]). A product has a slug id, a name, a description, a price in whole cents ([[0003-money-in-whole-cents]]), a stock count and an optional image.

Reading

Shoppers see only active products. GET /products/:id answers 404 for a product that is inactive, the same as for one that does not exist, so a retired product disappears from the shop without breaking old links into an error page.

Paging

GET /products returns active products by name, then id, so the order is stable when two products share a name. It returns a page at a time:

GET /products?limit=24
{ "products": [...], "next": "WyJMaW5lbiB3b3JrIGFwcm9uIiwibGluZW4tYXByb24iXQ" }
GET /products?limit=24&after=WyJMaW5lbiB3b3JrIGFwcm9uIiwibGluZW4tYXByb24iXQ

limit is 1 to 100 and defaults to 24, one catalog page. next is null on the last page. The cursor is the name and id of the last product on the page, base64url encoded; clients should pass it back unchanged and not build their own.

A cursor rather than ?page=3 because the query starts right after the last row ((name, id) > (?, ?)) on the products_by_name index, so page 40 costs the same as page 1, and a product added or retired between two requests does not shift the next page by one.

Writing

The merchant writes products with PUT /admin/products/:id and the admin token. The call is an upsert: the same request creates the product or replaces its fields, which makes it safe to retry and easy to drive from a spreadsheet export. Validation lives in parseProductInput:

  • the id is a lowercase slug of at most 64 characters, because it appears in URLs and image keys;
  • price_cents is a whole number above zero;
  • stock is a whole number, zero or more (the table also has a CHECK for it);
  • active defaults to true.

To retire a product, set active: false. Rows are never deleted, because cart items and orders refer to them.