Suppliers

4 min read

The one interface anything that makes or sends a thing wears, and how a site adds its own.

A supplier is anything that makes or sends what a shop sells: a print-on-demand press, a garment printer, a warehouse, the shelf in the back room, a file in a bucket. There is one interface, one registry, and nothing outside _shared/suppliers/ names a supplier by id.

A supplier makes a variant out of artefacts

That is the whole vocabulary, and it is deliberately empty of any word belonging to one kind of thing:

  • chosen — the variant's own option values: what the buyer picked.
  • artefacts — whatever the kind derived for that variant: a book's page count and its print files, a garment's artwork and its placements. The supplier understands them and nothing between the two needs to.

The interface used to take pages and files by name, which meant a supplier making anything but a book could not implement it — "plug-in" was true only of printers.

The interface

Field What it is
id a slug naming this supplier; the only place it is ever written
name what a person calls it
makes the formats it can make; [] for one that only sends
sends whether anything physically travels
stateful whether it keeps a title of its own, made once and replaced free after
keys the environment keys it cannot work without, by name
needs the artefacts it needs a kind to have produced
levels our three shipping words (standard, priority, express) in its own

keys and needs answer two different questions and a supplier that confused them would report LULU_CLIENT_KEY as a missing artefact. keys is how a deploy refuses before it starts; needs is matched against a kind's produces when the shop is configured.

Method What it answers
configured() / mock() keys present; every call answered locally
options(format) the shop's choices about how a thing is made — the admin's tab
catalogue(chosen) the buyer's axes, where the supplier publishes them. Optional
product({ chosen, options }) its own descriptor for that variant. Pure
measure({ product, artefacts }) a shape, where it has one
quote({ product, artefacts, quantity, country, level }) { print, shipping, currency, days }
validate({ product, artefacts }) { ok, errors }
register({ product, artefacts, title, sku, existing }) stateful suppliers only
order({ product, artefacts, ref, title, quantity, address, email, level, externalId, callbackUrl }) { id, status, cost }
status(id) / statusOf(word) its words onto ours: submitted · in_production · shipped · rejected · canceled · failed
webhook(body) / ensureWebhook(url) the job id a callback names; registering the callback

A supplier that sends nothing — a download — answers order by saying it is already done. One that is a shelf in a back room says so too, and waits for somebody to mark it sent.

quote has three answers, and they are different

A number is a price. null means ask somebody else — a hand bindery that quotes by eye. A real zero means nothing: a file costs nothing to send and nothing to make again. Collapsing the last two is how a shop ends up offering free shipping it cannot do.

The ones the kit ships

Id Makes Keys
lulu softcover · hardcover LULU_CLIENT_KEY, LULU_CLIENT_SECRET (+ LULU_SANDBOX)
mixam softcover · hardcover MIXAM_USER, MIXAM_PASS (+ MIXAM_UK)
bookvault softcover · hardcover · premium BOOKVAULT_API_KEY
post-room — (it only sends) none
printify shirt · hoodie PRINTIFY_API_KEY, PRINTIFY_SHOP_ID
download pdf · epub none

Every one is mocked by PRINT_MOCK=1, by its own <ID>_MOCK, and by having no keys — which is what lets the whole chain run on a fresh clone with no account.

A catalogue, where the supplier publishes one

Most publish none: a printer makes what it is sent and has no opinion about colours. Where one does, the axes are that supplier's to say, and a shop that types them in by hand is a shop that goes on selling a colour nobody makes any more.

So a product names its supplier, and Read the catalogue again in the admin writes what comes back into the file as ordinary option rows marked from: <supplier id>. Everything afterwards — the variants, the keys, the admin's grid, the catalog, the checkout's own check — reads them exactly as it reads a row somebody typed.

options:
  - name: Colour
    from: ridgeway
    values: [{ value: black, label: Black }, { value: slate, label: Slate blue, gone: true }]

A value the catalogue drops is marked, never deleted. Deleting it would be wrong twice over: a basket or an unfulfilled order names that variant and would stop resolving, and the shop is never told it has stopped selling something. So it stays with gone: true, every combination holding it is off sale at once, the till refuses one, and an order already placed still reads as the words the buyer chose. A value the catalogue brings back loses its mark and sells again, with nothing to clean up — which is why the mark is the mechanism rather than a list of dead keys the file would have to remember the provenance of. An axis the catalogue drops whole is kept the same way, because removing it would renumber every key the offer has.

The shop's own unsold list is never written by a refresh: that is a decision, and gone is news.

Adding your own

One file, and its name in your site's config:

// platformkit.config.js
export default {
  store: { suppliers: './build/suppliers.mjs', kinds: './build/kinds.mjs' },
}

Export a factory taking env (or a plain object) for each one. The registry walks the kit's and yours as one list, refuses a supplier that does not keep the shape by name at registration rather than halfway through a sale, and refuses two suppliers sharing an id. Nothing downstream knows which list yours came from.

sites/demo/build/suppliers.mjs has two worked examples: a hand bindery with no API at all, and a garment printer that publishes a catalogue.

Asking for them

packages/kit/build/suppliers.mjs is the one build-side door — suppliersFor(ctx) and kindsFor(ctx), both bound to the process's environment and cached per site root. Everything that needs a supplier asks there. Reaching past it to _shared/suppliers/index.js gets the kit's five and silently loses the site's, which is the shape a plug-in seam rots into: true of the interface, false of the product.

Kinds, the other half

A kind owns everything true only of one sort of thing, and reaches the store through exactly two slots: options(offer) and artefacts(variant). The store never opens the parcel — it stores the record, hands it to a supplier, and may ask only whether the variant is makeable yet.

A kind declares which artefacts it produces; a supplier declares which it needs; the two are matched when a shop is configured rather than when somebody tries to buy, because a refusal at the till is a refusal too late. An ordinary product registers no kind at all — not a null column, not a branch: absent.

The kit ships one: apparel, which owns everything true only of a garment — the product type, the design and where it sits, and a mockup per colourway — and reaches the store through those two slots and nothing else. See Selling garments.