Catalogue

Suppliers, products and in-house preparations — the reference lists the rest of the record points at.

The catalogue is what a restaurant keeps a list of rather than measures: who delivers, what comes through the door, and what the kitchen makes itself. Nothing here is a check or a reading. These are the rows everything else names, so they are usually the first thing you load and the last thing that changes.

CollectionScopeWhat a record is
supplierssuppliers:readA company that delivers, and how to reach it.
productsproducts:readSomething the restaurant receives and uses, by name.
preparationspreparations:readSomething the kitchen makes, with its shelf life and allergens.

All three share the snapshot envelope: id, deleted, capturedAt, receivedAt, sequence and the rest sit beside the data described below.

suppliers

One record per supplier the restaurant has entered, whether or not anything has arrived from them lately.

FieldTypeMeaning
namestring, requiredThe supplier as the restaurant names it. Free text, not a company register.
isDeletedboolean, requiredThe app's own retirement flag. Not the envelope's deleted — see below.
contactMethodsobject[]How to reach them. Each entry is { "type": string, "value": string }.
accountNumberstring | nullThe restaurant's account with this supplier, when one was entered.
{
  "id": "1f3c2a76-5b94-4b0e-9c1a-6d8f0a2e7b31",
  "collection": "suppliers",
  "deleted": false,
  "capturedAt": "1789431600214",
  "receivedAt": "1789431604771",
  "sequence": "412",
  "data": {
    "name": "Metro Nanterre",
    "isDeleted": false,
    "contactMethods": [
      { "type": "phone", "value": "+33 1 41 20 30 40" },
      { "type": "email", "value": "commandes@example.test" }
    ],
    "accountNumber": "FR-884213"
  }
}

contactMethods[].type is not an enum. The schema says string and stops, and the values above are an illustration rather than a list. Switch on it with a default branch, and show the value whichever way the type is spelled.

A delivery embeds its supplier inline as { id, name } — enough to put on screen, and nothing more. This collection is where the account number and the phone number live.

products

What the restaurant receives and uses. The record is as thin as it looks.

FieldTypeMeaning
namestring, requiredThe product as the restaurant named it.
isDeletedboolean, requiredThe app's retirement flag. See below.
{
  "id": "6b0d4ea2-91c7-4f3d-a0be-2c5f7d1e8a44",
  "collection": "products",
  "deleted": false,
  "capturedAt": "1789431980551",
  "receivedAt": "1789432001903",
  "sequence": "413",
  "data": {
    "name": "Beurre doux 250 g",
    "isDeleted": false
  }
}

That is the whole record: no reference, no category, no unit, no supplier. If your model needs any of that, it is yours to hold, keyed on the envelope's id.

Nothing else points at a product by id. A delivery records what was measured as temperatureRecords[].product, free text typed during the check — see deliveries. Matching that to this list is string work on your side, and "Beurre doux 250 g" is not "beurre doux". Do it if you must, but do not present the result as a join.

preparations

Something made in-house rather than received — a sauce, a stock, a terrine.

FieldTypeMeaning
namestring, requiredThe preparation as the kitchen names it.
lifespannumber, requiredHow long it keeps. A bare number.
allergensstring[]What it contains. Free strings, not a closed set.
isDeletedboolean, requiredThe app's retirement flag. See below.
{
  "id": "c47e8b13-0a52-4d96-8f1b-73ae2905cd6f",
  "collection": "preparations",
  "deleted": false,
  "capturedAt": "1789455120087",
  "receivedAt": "1789455133642",
  "sequence": "418",
  "data": {
    "name": "Sauce béarnaise",
    "lifespan": 3,
    "allergens": ["oeuf", "lait"],
    "isDeleted": false
  }
}

lifespan carries no unit. The schema gives you a number and nothing to interpret it with. Do not print "3 days" because three looks like days — confirm what the app means by it for the restaurant you are reading, and label it plainly if you cannot.

allergens is a list of strings, not a regulatory list. Entries come from the app in the language of the restaurant. Do not map them onto the fourteen named allergens without checking what the strings actually say, and do not treat an empty array as a cleared allergen check — it also means nobody filled it in.

isDeleted is not deleted

Every collection on this page carries both, they mean different things, and a consumer that filters on one of them gets a wrong list.

  • deleted, on the envelope, is the shadow copy's tombstone. true means the record was deleted in the app and data is gone. It is how you learn something went away — the envelope page has the rules.
  • isDeleted, inside data, is the app's own soft-delete flag on the row. The record is still there, still has its name, and still arrives with deleted: false. The restaurant retired it: it stopped offering it in pickers, and kept it so old records still resolve.

So filter on both. Honour deleted alone and your supplier list fills up with suppliers the restaurant dropped two years ago. Honour isDeleted alone and you keep rows that were deleted outright, because a tombstone has no data to read the flag from.

Keep the retired rows rather than dropping them, though. A delivery from last March still names a supplier that is isDeleted: true today, and resolving that name is the whole reason you hold this list.

Reading the catalogue

These three are small, slow-moving and needed by almost everything else, which makes them the cheap part of a sync and the easy part to get subtly wrong.

  • Walk them first, keep them, refresh them on a schedule. A restaurant's whole catalogue is a handful of pages at limit=100. Hold it in your own store keyed on the envelope id, and resolve names from there rather than fetching per record.
  • Names change, ids do not. name is free text the restaurant edits during service. Store the id as your key and treat the name as a label you refresh — never as something to join on.
  • Absence is ambiguous here too. A supplier that does not appear may never have been entered, or may not have reached us yet. See the caveat before you report a count.

Last updated 2026-09-20.