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.
| Collection | Scope | What a record is |
|---|---|---|
suppliers | suppliers:read | A company that delivers, and how to reach it. |
products | products:read | Something the restaurant receives and uses, by name. |
preparations | preparations:read | Something 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The supplier as the restaurant names it. Free text, not a company register. |
isDeleted | boolean, required | The app's own retirement flag. Not the envelope's deleted — see below. |
contactMethods | object[] | How to reach them. Each entry is { "type": string, "value": string }. |
accountNumber | string | null | The 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The product as the restaurant named it. |
isDeleted | boolean, required | The 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The preparation as the kitchen names it. |
lifespan | number, required | How long it keeps. A bare number. |
allergens | string[] | What it contains. Free strings, not a closed set. |
isDeleted | boolean, required | The 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.truemeans the record was deleted in the app anddatais gone. It is how you learn something went away — the envelope page has the rules.isDeleted, insidedata, is the app's own soft-delete flag on the row. The record is still there, still has its name, and still arrives withdeleted: 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 envelopeid, and resolve names from there rather than fetching per record. - Names change, ids do not.
nameis free text the restaurant edits during service. Store theidas 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.