Pink Salt Range: a wholesale salt business, back office and all, on one Worker
Most storefronts stop at the cart. This one carries the half of the business that comes after it: 32 tables covering suppliers, purchase orders, containers, lots, landed costs, shipments, invoices and quotes, served from 51 file-based routes on a single Cloudflare Worker with a D1 database. A buyer can pull the certificate of analysis for the exact lot their order shipped from.
The problem
A commodity wholesaler's website is the least interesting part of the business. The interesting part is that a container arrives, gets split into lots, each lot acquires a landed cost that is not the invoice price, and each lot has a lab certificate that a buyer in a regulated channel will eventually ask for by number. Most small distributors run that half on spreadsheets and the website half on a hosted store platform, and the two never meet. The consequence is routine and expensive: the price shown to a buyer is derived from a cost nobody has recomputed since the last freight change, and when a food-grade customer asks which lot their pallet came from, somebody opens a folder of PDFs. We built the storefront and the back office as one application so that neither question needs a human to answer it.
One Worker, one database, no separate backend
The whole application is server-rendered React on TanStack Start, compiled to a single Cloudflare Worker with the client assets bound alongside it and a D1 database bound as DB. There is no Node server, no separate API service and no second deployment target: one wrangler deploy ships the storefront, the back office and the schema migrations together. That shape is the reason the two halves can share a data model at all. A storefront on a hosted platform and a back office in a spreadsheet cannot share a definition of what a lot costs. Here the price a visitor sees and the landed cost an operator entered are two queries against the same tables.
One Worker, one database, no separate backend — specifics
51 file-based routes, including the storefront, the learn section, the legal set, an operator console at /ops and a token-addressed customer portal at /account/:token
18 SQL migrations in app/migrations, applied by wrangler against D1 rather than by hand
A daily scheduled trigger at 15:00 UTC drives the nurture pass
Three local packages under app/packages are consumed by the app rather than published
The data model is the wholesale business, not the website
The 32 tables divide into roughly three groups, and only one of them is what a store template gives you. Commerce: items, stock, orders, payments, price_tiers, subscribers. Supply: suppliers, purchase_orders, po_lines, containers, lots, lot_shipments, landed_costs, sku_costs, shipments, shipment_lines. Trade: quotes, quote_requests, quote_lines, invoices, invoice_lines, deals, prospects, contacts, outreach_events, attribution. The supply group is what makes the pricing honest. A landed cost row exists per lot, so the cost of goods reflects the freight and duty actually paid on the container that lot came out of, not the supplier's invoice line. The trade group is what makes a wholesale enquiry a tracked object rather than an email.
The data model is the wholesale business, not the website — specifics
coa_documents ties a lab certificate to a lot, and /lot/:code/coa streams it for that lot
portal_tokens addresses a customer's account by token, so a buyer reaches their documents without an account to create or a password to lose
rate_limits and suppressions are tables rather than middleware config, so the limits and the opt-outs survive a redeploy
A packing document route, /doc/packing/:shipment, renders from shipment_lines rather than being typed out
The custom domain is configured but not pointed
wrangler.jsonc declares pinksaltrange.com and www.pinksaltrange.com as custom domains on this Worker. The DNS for that domain does not resolve to Cloudflare today — the apex serves a 114-byte registrar parking page — so the application is reachable at its workers.dev address and nowhere else. This is stated rather than hidden because it is the difference between a thing that is built and a thing that is open for business, and the portfolio is not allowed to blur that. It is one DNS change, not a build task.
The numbers, and the command behind each one
File-based routes: 51 — ls app/src/routes/*.tsx app/src/routes/*.ts | wc -l
Tables in the D1 schema: 32 — grep -ohiE 'create table (if not exists )?[a-z_]+' app/migrations/*.sql | awk '{print $NF}' | sort -u | wc -l
Deployment targets: 1 — wrangler.jsonc main — dist/server/server.js, assets bound as ASSETS, D1 bound as DB
Landed cost is the part that does not want to be a table
A landed cost is not a number that belongs to a product. It belongs to a lot, which belongs to a container, which arrived on a shipment that carried several lots and one freight invoice. Splitting that invoice across lots is an allocation choice, and whichever rule you pick, the result is that a SKU's cost of goods changes when a new container lands rather than when anyone edits a price. The modelling consequence is that sku_costs cannot be the source of truth; it has to be derived from landed_costs per lot and whatever stock remains from each. That is a straightforward query and a treacherous one, because a wrong answer looks exactly like a right answer until margin is reconciled at the end of a quarter.
Where it bites
The allocation rule is in the migrations and the server functions, not in a spreadsheet somebody maintains
Stock is tracked per lot, which is what makes a certificate of analysis answerable per order
A quote's price tier and the cost it is measured against are both queries, so a stale cost shows up as a bad margin rather than a silently wrong quote
What this entry does not claim
We are not claiming revenue, orders, customers or traffic for this business. Every figure above describes the application and its schema.
The custom domain is not live. pinksaltrange.com serves a registrar parking page today; the application is reachable only at its workers.dev address, and the entry above says so.
Thirty-two tables existing is not the same as thirty-two tables carrying production data. The schema supports the whole wholesale flow; which parts are populated is a question about the business, not the build.
The certificate-of-analysis route streams a document that was supplied for a lot. We are not claiming any laboratory relationship, accreditation or testing of our own.
No food-safety, HACCP or regulatory certification is claimed for the business or the software.
Stack and status
Built with TanStack Start, TanStack Router, React 19, TypeScript, Cloudflare Workers, Cloudflare D1, Tailwind CSS 4, Zod, GSAP and three / React Three Fiber. Relationship: Our own company. Stage: In production. Period: Jul–Sep 2026. Our role: Whole application — storefront, data model, back office, deployment Figures re-derived on 2026-11-13.