Freight Quote Tool · Technical Architecture

Mini TAD — Freight Quote Tool

Pete · companion to the mini PRD · 2026-06-12

Shape

One Cloudflare Worker serving a small static UI plus a JSON API, behind Cloudflare Access. No installs, one URL.

Browser → single page, two input modes (item lookup | manual carton specs)
   │  JSON
   ▼
Cloudflare Access (email OTP, ~14-day session, named addresses)
   ▼
Cloudflare Worker
   ├─ product lookup → D1 table loaded from the distributor-catalog export
   ├─ carton math (ceil(qty ÷ units/carton), weights)
   ├─ UPS OAuth (client credentials, token cached until expiry)
   └─ UPS Rating API → "Shoptimeintransit" → all services, negotiated rates + transit
Secrets: Worker secrets (UPS client id/secret, account number) — never in source or browser

This is deliberately a re-skin of Megan's orchestrator minus the scraper and the desktop packaging: her module boundaries (lookup → carton math → rate → display) carry over; the things that ate both her sessions (scraping, Windows packaging, per-machine credentials) are structurally absent.

Components

1. Front end. Single page, plain HTML/JS. Two entry modes matching the PRD:

  • Item lookup: part #, quantity, ZIP, residential/commercial toggle. Shows the carton specs it found (with the option to override them — which is just manual mode pre-filled).
  • Manual: carton weight + dims (+ carton count), ZIP, residential/commercial.

Output: one table sorted by cost — service name, negotiated cost, transit days / expected delivery. Items without dims in the catalog get a clear nudge into manual mode.

2. API routes.

  • GET /api/item/{part} → carton specs (drives the show-what-we-found UI).
  • POST /api/quote{part | specs, qty, zip, residential} → rate table.
  • POST /api/catalog (admin-gated) → upload catalog .xlsx; parsed server-side → D1.

3. Product table. The distributor-catalog export: col B = ItemNum; DH/DI/DJ = carton L/W/H; DK = weight/carton; DL = units/carton. ~465 rows — trivial at this scale. D1 over KV so a future quote log is a table away, not a re-architecture.

Load/refresh mechanics: initial seed on build day via the same POST /api/catalog path the button uses. Each upload is parsed server-side (SheetJS), header-validated against the expected column map (clear error naming the missing/moved column, nothing ingested on failure), then swapped in atomically — stage into a fresh table, verify row count, rename. Catalog metadata (uploaded-at, by whom, row count, items missing dims) stored alongside and surfaced in the UI as "catalog last updated."

4. UPS integration.

  • OAuth 2 client-credentials against the UPS production environment; token cached.
  • Rating API with request option Shoptimeintransit — one call returns all available services with cost and time-in-transit, which is exactly Megan's "return the transit time and cost for all available service options."
  • NegotiatedRatesIndicator + the shipper's UPS account; ship-from ZIP fixed at 46514; ResidentialAddressIndicator when the toggle is set.
  • If negotiated rates come back absent, show published rates with a visible marker rather than failing silently.
  • The known gate: negotiated rates require the developer app to have Rating product access and the shipping account linked — almost certainly the "permission" Megan's rating call died on. Validated hour 1 of build day with a bare-bones script before anything else is built.

5. Auth. Cloudflare Access policy on the hostname: named emails, one-time PIN, ~2-week session. Admin route (catalog upload) behind a second, tighter policy (an Access group containing the admin emails).

User load/refresh mechanics: the allow-list lives in the CF Access policy, not in app code. Initial list seeded from Megan's ~5 emails (bulk ask) on build day. Adds/removes via the CF dashboard or the CF API — our agents can do it on request, same-day turnaround; no self-serve user admin in v1. The app itself never sees passwords or manages identity; it just trusts the Cf-Access-Jwt-Assertion header for "who is this" (used for the admin gate and the uploaded-by stamp).

6. No logging in v1 (PRD decision). If revived: a D1 quotes table + a "download CSV" button — about an hour.

Credentials & hygiene

  • UPS client id/secret and account number live as Worker secrets (wrangler secret put), set at deploy; nothing in git, nothing client-side.
  • No Azure/SharePoint surface in v1 at all.
  • At credential handoff: rotate the UPS keys as a standard precaution. Raw exports stay out of git — standing rule.

Repo & deploy

  • Private GitHub repo; wrangler deploy. Stack is industry-standard and boring on purpose — any competent shop, or Keystone's own team post-handoff, can pick it up and keep it running. You're never locked in.
  • Open decision: deploy into Rogue Agents' Cloudflare vs. a Keystone-owned account (6/10 principle says client-owned, RA-configured). Either way the move later is DNS + secrets, not a rebuild. This also ladders into the order-entry stack if that deal lands — same account, same patterns, nothing thrown away.

Build-day plan

The build-day sequence, timeline, and the input checklist were tracked in a companion implementation plan.

Assumptions (over-reported on purpose; Nate filters)

  • One product table serves both brands (confirmed 6/11).
  • Catalog export dims are inches / pounds.
  • Partial last carton is rated as a full carton (conservative; matches her original carton-count math). Flag if they'd rather pro-rate.
  • Origin is always 46514; no multi-origin, no Saturday-delivery or drop-off-location options surfaced as needs.
  • UPS Rating API is free; 25–30 calls/day is negligible for every quota involved.
  • ~5 named users; CF Access free tier (50) is years of headroom.