Part 4 — Running It in Production
R The Trade Show Playbook
Three days in a convention hall decide a large share of a season's revenue, and they happen on the worst network you will ever ship software onto. This chapter is the operations manual: why venue Wi-Fi and cellular actually fail, the exact hardware kit with power math that survives a nine-hour day, a countdown runbook from three weeks out to the morning bell, the booth flow for writing and reviewing orders, nightly reconciliation, and a failure playbook for the eleven things that genuinely go wrong. Carry it. Mark it up.
In this chapter
What you need to know first
This chapter mixes network engineering, hardware logistics and apparel sales practice. The minimum vocabulary follows, defined from nothing, so no sentence later depends on a word you have not met.
Shows, booths and showrooms
A trade show is a temporary market. A show organizer rents a convention hall, sells floor space to brands, and invites retail buyers to walk it. For wholesale apparel the big ones run twice a year, matched to the selling seasons described in chapter D.
Your rented floor space is a booth: typically a 10 ft × 10 ft or 10 ft × 20 ft rectangle with pipe-and-drape walls (metal poles holding fabric panels, which is what passes for a wall in a convention hall), hanging rails for samples, and whatever furniture you paid the venue to deliver.
A showroom is the permanent version of the same thing, your own room in a fashion district building, and everything in this chapter applies there too, except that showroom network problems are yours to fix and show-floor network problems are not.
Market week is the cluster of shows and showroom appointments where a season gets sold. Order writing is recording what a buyer commits to: which styles, colorways and sizes, how many, at what price, shipping in which window.
Your side is a rep (sales representative); the other side is a buyer, who works for a retailer and holds an open-to-buy budget — a fixed pot of money for the season, described in chapter E. Once that pot is spent it is spent, which is why an order lost to a broken iPad is usually not recoverable later.
Styles, SKUs and delivery windows
The product words come next, because every query and screen below uses them:
- A style is one design, "the denim jacket", with a style number.
- A colorway is one color version of that style: indigo, black, olive.
- A size run is the set of sizes it is made in, say XS to XL.
- A SKU (stock keeping unit) is one exact combination of style, colorway and size — "denim jacket, olive, medium", and it is the thing you actually count in a warehouse.
- An assortment is the set of styles you have chosen to sell at a particular show.
- A delivery window is the pair of dates an order ships between: a ship start date, and a cancel date after which the buyer is entitled to refuse the shipment.
- Move-in is the period before a show when exhibitors are allowed into the hall to build their booths; load-out is the reverse, at the end.
Linesheets, prepacks and available-to-sell
Three more sales words you will meet below:
- A linesheet is the one-page-per-style price document a rep sells from: photo, style number, colorways, size run, wholesale price and delivery window.
- A lookbook is the styled photography version, meant to sell the feeling rather than the numbers.
- A prepack is a pre-decided bundle of sizes sold as one unit — say two smalls, three mediums, two larges and one extra-large, so a buyer orders "four prepacks" instead of filling in eight boxes.
ATS means available-to-sell: how many units of a given size-and-color you can still promise after subtracting what is already committed. Chapter 8 builds it properly. What matters here is that ATS is a shared, contended number — if two people subtract from it independently you get a wrong answer that looks completely normal.
Bandwidth, backhaul and captive portals
A few network terms:
- Bandwidth is how much data can move per second; latency is how long one round trip takes. A trade show usually has adequate bandwidth on paper and terrible latency in practice, and latency is what makes software feel broken.
- An access point is the box on the ceiling that broadcasts a Wi-Fi network; a hall has many of them.
- Backhaul is the wire that carries traffic from those access points out to the internet — the whole hall shares it, so it is a second place congestion can happen even when the radio side is fine.
- An SSID is the name of a Wi-Fi network — the thing you tap in the list.
- DHCP is the automatic system that hands your device an address when it joins a network; if the network runs out of addresses, your device joins and then silently cannot talk to anything.
- A captive portal is the login page a venue forces you through before it lets your traffic out — hotel and airport Wi-Fi all work this way.
- A hotspot (or MiFi) is a small battery-powered box with a SIM in it that turns a cellular signal into a private Wi-Fi network for your devices; tethering is the same thing done by a phone.
APIs, JSON, TLS, SQLite and tokens
Five software words, because the code later uses them:
- An API (application programming interface) is the set of web addresses your app calls to talk to your server; a person visits a web page, a program calls an API.
- JSON (JavaScript object notation) is the plain-text format those calls send data in — labeled values wrapped in braces, readable by a human if you squint.
- TLS (transport layer security) is the encryption that turns
httpintohttps: it scrambles traffic so nobody in between can read or alter it, and it proves the server you reached is the one you asked for. - SQLite is a complete database that lives in a single file on the device itself, with no server and no network — it is what lets an iPad hold your whole catalog in a hall with no signal.
- A JWT (JSON web token, pronounced "jot") is a small signed string your app sends with every API call to prove who it is; it carries an expiry time inside it, and once that time passes the server refuses it.
Chapter 6 uses all five.
eSIM, watt-hours and what flies in a cabin
eSIM is a SIM card built into the device as software rather than a plastic chip. Every current iPad is eSIM-only — Apple's specification page for the iPad (A16) states that "iPad uses advanced eSIM technology for more convenience and security (not compatible with physical SIM cards)" (apple.com, checked July 2026). That matters operationally: you cannot fix a dead cellular plan at the booth by borrowing someone's SIM.
Two battery units, and people confuse them constantly. mAh (milliamp-hours) is charge, and it is only comparable between two batteries at the same voltage. Wh (watt-hours) is energy, and it is comparable between anything. Always plan in watt-hours. The number is printed on every power bank because aviation rules are written in watt-hours.
US hazardous materials regulations for passenger aircraft (49 CFR 175.10) put the ordinary limit for a lithium ion battery at 100 Wh, permit batteries "exceeding 100 Wh, but not exceeding 160 Wh" only with the air carrier's approval, allow "no more than two" such larger spares per person, and require that "spare lithium batteries must be carried in carry-on baggage only" (49 CFR 175.10, checked July 2026). Individual airlines can still cap how many spare batteries you bring for personal use, so read your carrier's page too.
Device management and service levels
MDM means mobile device management: a service that configures company-owned devices remotely — installing apps, setting Wi-Fi profiles, locking a device into one app, wiping it if stolen. Supervision is Apple's stronger management mode for company-owned hardware. Automated Device Enrollment (ADE) puts a device into supervision automatically on first switch-on, provided you bought it through Apple or an authorized reseller and linked it to your organization in Apple Business Manager.
An SLA (service level agreement) is a written promise about response time, "we answer credit applications within five working days", and it is the number you quote to a buyer instead of guessing.
Create, don't mutate
Finally, the design idea this entire chapter rests on, from chapter 6: create, don't mutate. Offline devices are allowed to create new documents — orders, order lines, notes, prospect records — using identifiers they generate themselves. Offline devices are never allowed to change shared truth: no writing to inventory, no decrementing ATS, no editing another rep's order.
The iPad records what a buyer committed to. The server, running one transaction at a time, decides what that commitment does to inventory. Chapter 6 explains the reasoning. This chapter is what that reasoning costs and saves you in a hall with thousands of people in it.
Why trade shows break software
Every part of the environment is hostile in a different way, and each way has a mechanism you can name. Name them, because the failures look identical from the booth, "it's not working", and the fixes are completely different.
Wi-Fi: the airtime problem
Wi-Fi is a shared radio channel, and only one device on it can transmit at a time. Everything else waits, listens and retries. This is called CSMA/CA — carrier sense multiple access with collision avoidance. The practical consequence: a Wi-Fi channel behaves like a microphone passed around a room rather than a pipe you divide up. Two hundred devices on one access point do not each get 1/200th of the speed. They queue for the microphone, and every failed grab costs everyone a slice of silence.
A second effect compounds that. Plain Wi-Fi shares out turns to transmit. Every device gets roughly the same number of turns, however much data it moves in one turn. A device on a weak, noisy signal negotiates a slow encoding, so sending the same 100 KB takes it ten or twenty times longer than a device sitting next to the access point, and while it transmits, nobody else can.
Researchers named this effect the 802.11 performance anomaly, and modern access points fight it with an "airtime fairness" feature that you have no way of knowing whether the venue enabled. One badly-placed laptop at the far end of a hall degrades everyone on that channel, and a hall holds hundreds of them, plus every attendee's phone and every neighbouring booth's hotspot.
Then there is spectrum. The 2.4 GHz band has only three non-overlapping channels in most of the world (1, 6 and 11), shared by every hotspot, wireless microphone, Bluetooth device and microwave in the hall. The 5 GHz band has many more channels and is far better, though coverage is shorter and many of its channels must yield to radar.
The 6 GHz band (Wi-Fi 6E and 7) is genuinely clean, and comparatively few devices at a trade show can use it yet — including the current base iPad, whose specification page lists "Wi-Fi 6 (802.11ax) with 2x2 MIMO" and no 6E (apple.com, checked July 2026). MIMO there just means the radio uses two antennas at once to move more data; the point for you is the missing "6E".
Why vendor client-count numbers mislead
Vendor capacity numbers mislead, and the vendors say so themselves. Cisco Meraki publishes per-radio maximums — 512 clients per radio for Wi-Fi 6, giving 1,024 per access point, and 1,536 per access point for Wi-Fi 6E and 7, and then states that "the above list is only a theoretical maximum; in real-world deployment, interference caused by multiple clients communicating simultaneously will cause this limit to be far lower", adding that just because an access point "might theoretically handle 512 or 1024 maximum clients, this does not mean it is something we want to design for" (Meraki documentation, checked July 2026).
A show floor is the worst case for every limiting factor: unknown client mix, unknown application mix, and a backhaul link shared with the whole hall.
Two more failure modes you will hit. DHCP pool exhaustion: the venue sized its address range for an average show, a busy day fills it, and new devices associate successfully and then get no address — full bars, Wi-Fi icon, nothing works.
Client isolation: most venues deliberately stop guest-Wi-Fi devices seeing each other, which is correct security practice and also means your iPad cannot reach your network printer on the same SSID.
Captive portals: why the app hangs instead of failing
A captive portal intercepts your traffic and redirects it to a login page until you accept terms, enter a code or pay. The standards body describes the classic implementation in exactly those terms. RFC 8910 says that to show users a payment or acceptable-use page, "a captive portal enforcement device presently has to intercept the user's connections and redirect the user to a captive portal server, using methods that are very similar to man-in-the-middle (MITM) attacks", and warns that "as increasing focus is placed on security, and end nodes adopt a more secure stance, these interception techniques will become less effective and/or more intrusive".
How a device discovers a portal
The modern approach is RFC 8910 plus RFC 8908. The network advertises a captive portal API address through a DHCP option (code 114 for IPv4) or an IPv6 router advertisement — the message a modern network broadcasts to tell devices how to configure themselves. The device fetches that address over an encrypted connection and gets back a small JSON document, served with the label application/captive+json.
That document has one required field, a captive boolean (true or false: are you locked out?), plus optional fields including user-portal-url, venue-info-url, can-extend-session, seconds-remaining and bytes-remaining.
Many venues do not implement any of this, so devices fall back to probing: iOS requests a known page on captive.apple.com and expects a tiny HTML page whose only visible word is Success — fetch https://captive.apple.com/hotspot-detect.html right now and you will get exactly that. Anything else — a redirect, a login page, a blank 200, and iOS decides the network is captive and pops the login sheet.
A portal is worse for your app than no network at all, for a mechanical reason. Your HTTPS request to the server does not get refused. The portal answers on the server's behalf and accepts the TCP connection — the initial handshake that opens a two-way channel between two machines, so your socket, the program's end of that channel, opens normally.
Then either the TLS handshake fails because the portal presents its own certificate, or the connection sits there while the portal waits for a login. A client with no timeout waits, in practice forever, and the rep sees a spinner while a buyer stands in front of them.
The single most damaging network state at a trade show is the half-open one: Wi-Fi associated, address assigned, socket opens, no data returns. Your device thinks it is online, so it stops trying alternatives, and any code path that waits on the network hangs instead of failing. Every network call your app makes must have a hard timeout, and the app must treat a timeout exactly like being offline. Test this specifically — join a captive network, do not log in, and use the app for ten minutes.
Idle timeouts and private Wi-Fi addresses
Plan for two more portal behaviors. Portals commonly re-authenticate on an idle timer, so a device that sat in a bag over lunch is captive again at 2 pm without anyone touching it.
And portals bind sessions to a MAC address, the network adapter's hardware identifier, which collides with iOS's private Wi-Fi address feature, where the device presents a randomized identifier per network; a portal can see a "new" device and demand login again.
If you use venue Wi-Fi at all, turn off private Wi-Fi address for that SSID on each device before the show. It is a per-network setting, so you set it once per device, and you should turn it back on afterwards because it exists to stop venues tracking your staff between visits.
Cellular: thousands of radios in one hall
"Just use cellular" is the instinctive answer and it is only half right. Cellular at a crowded event degrades in a measurable, documented way, and the degradation is worst in exactly the direction you need.
A cellular device cannot simply transmit. It must first be granted radio resources through a negotiation the standards call RRC (Radio Resource Control), which starts with a contention-based random-access attempt on a shared channel. When thousands of devices in one building grab at once, those attempts collide and the network starts refusing them.
Researchers from Michigan State University and AT&T Labs instrumented the network of a US tier-1 operator, one of the handful of national carriers that own their own nationwide network, during two large crowded events in 2012, and measured it. Cell sectors closest to the venue, they found, "have 2-3 orders of magnitude more RRC failures on the event day than the routine day", and "the RRC failure ratios progressively decrease as the distance of cell sectors to the venue increases" (Shafiq et al., "A First Look at Cellular Network Performance during Crowded Events", SIGMETRICS '13).
A crowd is an uplink event
The same study measured load direction. Uplink — device to network, the direction an order upload travels — took the bigger hit: "the peak uplink throughput on the event day is more than 8x and 4x the average throughput on the routine day for events A and B, respectively", while "increases in the downlink throughput timeseries are steadier for both events" — downlink being the other direction, network to device.
Crowds generate uplink: photos, video, social posts, and every other exhibitor's point-of-sale system pushing against the same congested uplink your sync uses.
That paper measured a 3G network, and the radio technology has moved on twice since. Treat the numbers as evidence for the mechanism rather than a forecast for your show: 4G and 5G still start with a contention-based random-access procedure on a shared channel, and thousands of handsets in one hall still collide on it. What has not changed is that a crowd is an uplink event.
Large venues mitigate this with a DAS (distributed antenna system — carrier antennas in the ceiling) and COWs (cells on wheels — trailer-mounted temporary base stations outside). These help, and they are provisioned for the venue's expectation, not yours.
That leaves two consequences. Signal bars show signal strength, not congestion, so five bars and no throughput is normal at a show. And congestion is per-carrier, because each carrier has its own spectrum, antennas and backhaul, which is the entire argument for the second-carrier hotspot later in this chapter.
Power, connectivity, and the venue's price list
Convention halls have no wall sockets you may use. Power is a metered service ordered in advance from the venue's electrical contractor, priced per event, and expensive. So is anything that carries data. These are published numbers from the Jacob K. Javits Convention Center 2026 Exhibitor Price List, "effective for events held 1/1/2026 through 12/31/2026", read directly from the catalog PDF in July 2026.
| Service (Javits 2026 price list) | Advance rate | Standard rate |
|---|---|---|
| E025 — up to 500 W (5 A) / 110 V, show hours, one device | $147.50 | $211.00 |
| E025X — same, 24-hour power | $184.50 | $263.50 |
| E026 — 501–1000 W (up to 10 A) / 110 V | $243.75 | $348.25 |
| E026X — same, 24-hour power | $304.50 | $434.75 |
| E028 — multi box (power strip from the venue) | $54.65 | $78.05 |
| T508 — Wi-Fi for 1–4 devices, per device, up to 5 days | $262.75 | $350.00 |
| T509 — Wi-Fi, 5+ devices, per device, up to 5 days | $210.00 | $280.00 |
| T532 — Wi-Fi for card-processing devices, 5+, per device | $210.00 | $280.00 |
| T013 — credit-card processing Ethernet drop, 256 Kbps | $346.00 | $461.25 |
| T044 — 1.5 Mbps managed wired line (legacy T1) | $3,126.00 | $4,169.00 |
| T102 — 3 Mbps managed wired line | $4,561.00 | $6,081.00 |
| T073 — 5 Mbps managed wired line | $7,222.00 | $9,629.00 |
| T074 — 10 Mbps managed wired line | $13,542.00 | $18,056.00 |
| T105 — wireless router rental (requires a wired line first) | $301.00 | $401.25 |
Read the wired internet rows twice. A dedicated 3 Mbps line, slower than a phone on a good day, is $4,561 at the advance rate and $6,081 if you order after the advance deadline has passed. Add the router rental you need to turn that line into your own booth Wi-Fi ($301) and the power to run it ($147.50) and a private, venue-supplied booth network starts at $5,009.50, before labour.
That arithmetic is what makes offline-first the only sane architecture for a small brand. It is also why the advance-rate column matters: the standard rate runs about 43% above the advance rate on the electrical lines and about 33% above it on every telecom line.
Advance deadlines, card processing and router rental
Copy three details from that catalog into your own planning:
- The advance deadline is not printed on the price list — it is set per show by the organizer, so the only date that binds you is the one on your own show's exhibitor service kit, which arrives as a PDF with the deadline on the cover.
- The catalog also warns that "standard Wi-Fi isn't secure enough for credit card processing" and sells a separate configured service for card terminals, which tells you the house guest network is not somewhere to run payments.
- And the router rental line states plainly that you must already have ordered a managed internet service before it will do anything, which is the trap people fall into: they buy the router, arrive, and discover it has nothing to connect to.
Prices differ by venue, city and year. Treat these as an order of magnitude and pull your own show's kit. The non-obvious power fact holds everywhere, though: show power is usually switched off shortly after the hall closes, unless you paid for the 24-hour variant. Anything left charging in the booth overnight will not be charged in the morning.
One rule to confirm in writing before you plan around any of this: ask the organizer whether exhibitor-operated wireless access points are permitted. Some venues restrict or ban them, because a hall full of private hotspots degrades the house network for everyone. If your show is one of them, the hotspot layer below is not available to you and your offline layer has to carry the whole show on its own, which, if you built it properly, it can.
The people are the fourth failure mode
Reps at a show stand for nine hours, talk continuously, and carry a personal revenue number. They are not in a state to troubleshoot.
An unfamiliar confirmation dialog gets dismissed unread. A spinner gets tapped again, and again, creating duplicates. A required field they cannot answer gets "N/A" or "asdf".
Every one of those is a software design problem. Treating them as discipline problems — more training, a sterner email — has never worked, and every one of them is cheap to prevent and expensive to clean up afterwards.
Treat the iPad as a notebook. It can write down that a buyer committed to 200 units, because writing something down is safe to do twice, out of order, by any number of people at once — you just end up with more notes. Deciding how many units are left is a different job, and the iPad must never do it: two devices that each subtract 200 from 300 both compute 100, both are wrong, and nothing anywhere reports an error. Availability is decided in exactly one place, a single server transaction, one order at a time.
Connectivity strategy in layers
The strategy is a stack ordered by how much you control. Layer 0 is the only one you fully control, so Layer 0 has to be sufficient on its own. Everything above it makes the day nicer and reconciliation faster, and nothing above Layer 0 is ever load-bearing.
| Layer | What it is | You control | What it is allowed to be responsible for |
|---|---|---|---|
| 0 | Local SQLite on the iPad, full catalog and price list synced before travel | Everything | Writing orders, showing the line, pricing, totals, capturing prospects. The show can be sold with only this. |
| 1 | Your own 5G hotspot, carrier A, sitting high in the booth | Hardware, SIM, SSID, who joins | Background sync. Nothing user-facing waits on it. |
| 2 | Rep phone tether, carrier B (different from carrier A) | Hardware; plan is personal | Emergency sync of one device, or an urgent credit check. |
| 3 | Venue wired drop into a travel router, if you bought one | The router; not the line | Printer, manager's laptop, evening bulk sync. |
| 4 | Venue Wi-Fi | Nothing | Last resort. Assume captive, assume slow, assume it drops. |
Two rules turn this table into working software. One: the app never blocks on the network for anything a rep does in front of a buyer. Two: connectivity may change the status of something a rep already did. It must never decide whether they can do it at all.
Why the iPad may write orders but must never decide availability
The notebook analogy lands with non-engineers immediately. Two reps at opposite ends of the booth each have a notebook. Rep A writes "Bluebird Boutique: 200 units of the denim jacket." Rep B writes "Nine Streets: 150 units of the denim jacket." Both are true statements about what a buyer said, neither needs to know about the other, and when both notebooks are handed in you keep both facts.
Now suppose each notebook also held the running stock figure. Both start with 300. Rep A subtracts 200 and writes 100. Rep B subtracts 150 and writes 150. Whichever notebook is handed in last overwrites the other, and the system ends up holding a number that looks entirely reasonable and is wrong by hundreds of units. Nothing errors, nothing is flagged, and you discover it in six weeks when the warehouse cannot ship — chapter 6's exact warning.
So the iPad shows availability as an advisory snapshot with a timestamp: "about 300 available as of 07:40 today". The rep uses that to steer the conversation; the commitment happens server-side. If two orders jointly exceed stock, nothing fails at the booth at all. The second order lands in a review queue that evening, as a business decision for a human to make.
// lib/net.ts — every network call in the app goes through this.
// Nothing else is allowed to call fetch() directly.
export type NetOutcome<T> =
| { ok: true; value: T }
| { ok: false; reason: 'offline' | 'timeout' | 'captive' | 'server' };
const HARD_TIMEOUT_MS = 4000; // a buyer will not wait longer
const PROBE_URL = 'https://api.yourbrand.com/healthz';
export async function guardedFetch<T>(
url: string,
init: RequestInit = {},
): Promise<NetOutcome<T>> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), HARD_TIMEOUT_MS);
try {
const res = await fetch(url, { ...init, signal: ctrl.signal });
// Order matters. Classify by HTTP status FIRST. If you check
// the content type first, a 500 that renders an HTML error
// page gets reported as 'captive' and you go and blame the
// venue for your own outage.
if (res.status === 511) {
// RFC 6585 "Network Authentication Required": the status a
// well-behaved intercepting proxy is supposed to return.
return { ok: false, reason: 'captive' };
}
if (!res.ok) return { ok: false, reason: 'server' };
// A 2xx that is not JSON is a portal login page wearing a
// success code. Our API only ever answers with JSON.
const ct = res.headers.get('content-type') ?? '';
if (!ct.includes('application/json')) {
return { ok: false, reason: 'captive' };
}
return { ok: true, value: (await res.json()) as T };
} catch (e) {
const aborted = e instanceof DOMException && e.name === 'AbortError';
return { ok: false, reason: aborted ? 'timeout' : 'offline' };
} finally {
clearTimeout(timer);
}
}
// Reachability, used only to colour the status chip. Never awaited
// on a path that a rep is standing in front of.
export async function isReallyOnline(): Promise<boolean> {
const r = await guardedFetch<{ status: string }>(PROBE_URL);
return r.ok && r.value.status === 'ok';
}
What this does: it forces every request through one function with a four-second hard ceiling, using AbortController — the built-in JavaScript object for canceling a request that is already in flight, so the socket is actually torn down instead of left hanging. It then separates four outcomes that look identical to naive code:
offlinemeans the request never left.timeoutmeans something accepted the connection and did not answer, which is the captive-portal-or-congestion case.captivemeans something answered with a portal instead of your API.servermeans your API is reachable and unhappy, which is a completely different problem with a completely different fix.
Note the ordering inside the try block: status codes are judged before content type, because your own server's HTML error pages would otherwise be misread as a venue portal.
The reachability probe hits your own /healthz endpoint rather than a generic one, because a portal will happily allow the well-known probe addresses it has learned about — including captive.apple.com, while still blocking everything else. Make /healthz a public endpoint that needs no token, or the probe will report server on a perfectly good network.
What the reader should see when testing: joining a hotel Wi-Fi without logging in produces captive within four seconds, and airplane mode produces offline immediately.
The token expiry trap
This one bites teams on day two and it is invisible in testing unless you look for it. Supabase access tokens, the JWTs your app presents on every call, default to a one-hour lifetime, and Supabase's documentation says that "most applications should use the default expiration time of 1 hour" and that "setting a value over 1 hour is generally discouraged for security reasons".
Refresh tokens are the longer-lived credential that buys you a new access token. Supabase's are single-use, with a short reuse window so a legitimate double-call does not lock you out ("a refresh token can only be used once", with a documented ten-second grace interval), and they do not expire on a timer — the session lasts until sign-out or a termination event (Supabase docs, checked July 2026).
All of that is fine while offline, because you cannot sync anyway. The failure is different, and it is a startup-path failure: a rep force-quits the app on day two, reopens it with no usable network, the startup code asks the auth server to confirm the session, that call hangs or fails, and the app shows a login screen over a device holding 40 unsynced orders. The data is still in SQLite; the rep cannot see it; at the booth that is indistinguishable from total loss.
The trap has a specific cause, and it is easy to write by accident. Supabase's own reference for getSession() describes it as returning "the session, refreshing it if necessary" — that refresh is a network call. Any startup path that awaits getSession() before deciding what to draw has put the show-floor network on the critical path to the first screen.
// app/boot.ts — open the database first, authenticate later.
import { createClient } from '@supabase/supabase-js';
import { db } from './powersync';
import { secureStorage } from './secure-storage';
// SUPABASE_ANON_KEY is a PUBLIC key. It is designed to ship
// inside the app and is safe there, because row-level security
// decides what it can read. The service_role key is a different
// animal: it bypasses row-level security entirely, so it must
// never appear in an app bundle, a repo, a screenshot or a
// build config.
//
// As of July 2026 Supabase is moving to a new pair of names:
// "publishable" (sb_publishable_...) and "secret"
// (sb_secret_...). The old anon and service_role keys still
// work, but Supabase's docs say they will be deprecated by the
// end of 2026, so use the new ones on a new project.
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
persistSession: true, // survives force-quit
autoRefreshToken: true,
detectSessionInUrl: false, // no browser redirect on native
storage: secureStorage, // Keychain-backed on iOS
},
});
// supabase-js stores the session under this key. Read it directly
// so we can answer "is anyone signed in on this device?" with no
// network at all.
const SESSION_KEY = `sb-${PROJECT_REF}-auth-token`;
function hasRefreshToken(raw: string | null): boolean {
if (!raw) return false;
try {
return typeof JSON.parse(raw)?.refresh_token === 'string';
} catch {
return false; // corrupt storage is not a session
}
}
export async function boot() {
// 1. Open the local database FIRST. It needs no network and no
// valid access token. The rep can work immediately.
await db.init();
// 2. Decide the first screen from STORAGE, not from the auth
// server. Do NOT await supabase.auth.getSession() here: it
// refreshes the session if it has expired, and that refresh
// is the network call that hangs on a show floor.
const stored = await secureStorage.getItem(SESSION_KEY);
// 3. An EXPIRED access token is not a reason to lock anybody
// out. Only a device with no stored refresh token at all
// has genuinely never been signed in.
if (!hasRefreshToken(stored)) return { screen: 'login' as const };
// 4. Connect sync in the background. Failure here is normal and
// must never surface as a modal.
void db.connect({
fetchCredentials: async () => {
const { data, error } = await supabase.auth.refreshSession();
if (error || !data.session) {
// No token right now — almost always just "no network".
// Signal failure to the sync client and let it try again
// later. Whatever your SDK version does with a rejected
// fetchCredentials, it must never reach the rep's screen.
throw new Error('credentials unavailable');
}
return {
endpoint: POWERSYNC_URL,
token: data.session.access_token,
};
},
});
return { screen: 'booth' as const };
}
Two terms in that code, before the walkthrough. Row-level security is a Postgres feature that attaches rules to a table so each signed-in user can only read the rows the rules allow — it is what makes a public key safe to ship. The keychain is the encrypted store iOS gives every app for secrets, so the saved session survives a force-quit without sitting in a readable file.
The order of operations is the whole point. The local database opens before any authentication check, so the rep sees the catalog and their orders with the radio off.
The session check reads the keychain and stops there — it asks "has anyone ever signed in on this device?", which is a question storage can answer, instead of "is this token still valid?", which only a server can answer. A missing session sends you to login; an expired access token does not, because the refresh token in the keychain fixes that the moment there is a network.
Sync connects with void, deliberately un-awaited, so a hanging connection cannot delay the first screen. PowerSync calls fetchCredentials whenever it needs a fresh token, and that function is allowed to fail quietly for three days.
Session timeouts to disable before the show
One dashboard check before every show, under Authentication → Sessions. Supabase offers a time-box (sessions end after a fixed duration) and an inactivity timeout (sessions end if not refreshed within a window), both on the Pro plan and above. Either one will sign out a rep who has been offline for four days, the instant they reconnect. Disable them for the show window, or set them comfortably beyond its length, and write a calendar reminder to put them back.
Test that path explicitly before every show. Put a device in airplane mode, write five orders, force-quit the app, wait past your access token lifetime, and reopen with the radio still off. You must land on the booth screen with all five orders visible and marked pending. Landing on a login screen is a show-ending bug that you will otherwise discover in front of a customer.
Signing in at the booth, and why you never should
Every way of proving who you are needs the network at the moment of proof. That single fact decides your whole booth policy, so walk through the options a beginner will be offered:
- A password alone is checked by the server, so it needs the network.
- TOTP (time-based one-time password) is the six-digit code from an authenticator app: the app and the server share a secret, both compute a code from that secret and the current clock, and the codes match. The phone generates it with no signal at all, but the server still has to check it, so signing in still needs the network.
- WebAuthn (the standard behind passkeys) replaces the password with a private key held in the device's secure hardware and unlocked by Face ID or a fingerprint; it is much harder to phish, because the credential is bound to your exact domain and cannot be typed into a fake site. It also needs the server online to verify the signature.
- SAML and OIDC are the two standards behind "log in with your company account": OIDC (OpenID Connect) is the modern one, built on OAuth 2.0 and used by Google and Microsoft sign-in; SAML is the older XML-based one common in large enterprises. Both work by bouncing the user to an identity provider's website and back, so both need a working internet connection and a working browser view — the two things a convention hall is least likely to give you.
The policy that follows is short. Every rep signs in at the office, on the office network, at T−7, and nobody signs out until the order book is reconciled. Remove "Sign out" from the booth build, or bury it behind a manager passcode; a rep who taps it on Tuesday cannot get back in until Thursday.
During show hours your data is protected by the device passcode and Face ID, backed by MDM single-app mode so the iPad cannot leave your app. The application login protects nothing for those three days, because nobody logs in or out.
And one device per named rep, recorded on the device register — shared logins destroy the audit trail that every later section of this chapter depends on.
Should you run a server in the booth?
Someone will suggest it: a small machine in the booth running your API and database, so the iPads sync over the booth's own network with no internet at all. It sounds like the perfect answer to a dead hall. Usually it is the wrong answer, and the reasons involve terms you will meet everywhere else:
- A container is your application packaged with everything it needs to run — libraries, runtime, configuration — in one image that behaves the same on any machine.
- Docker is the common tool for building and running them.
- systemd is the service manager on most Linux systems: it starts your program when the machine boots, restarts it if it crashes, and captures its logs.
- A reverse proxy is a server that sits in front of your application, accepts requests and forwards them on; it is where TLS is normally terminated and where routing rules live, so the application itself never has to think about certificates. Caddy, nginx and Traefik are the usual choices.
Certificates cannot be renewed in a dead hall
Certificates are the part that catches people. To serve https, the reverse proxy needs a TLS certificate, and certificates are issued through ACME (automatic certificate management environment), the protocol Let's Encrypt uses. ACME makes you prove you control the domain, in one of two ways.
The HTTP-01 challenge has the certificate authority fetch a token file from your web server over port 80 — impossible from a convention hall, where nothing is publicly reachable. The DNS-01 challenge instead has you publish a TXT record (a DNS entry holding arbitrary text) at _acme-challenge.yourdomain; Let's Encrypt notes that with it "you can use this challenge to validate domain names whose webservers aren't exposed to the public internet", and it is the only Let's Encrypt challenge that can issue a wildcard certificate — one covering every subdomain at once.
But issuing or renewing a certificate by either method requires the machine to talk to the certificate authority, which means internet access. In a hall with no internet, a certificate that expires mid-show cannot be renewed, and every iPad will refuse to connect with a security error that no rep can diagnose.
So if you do this, issue the certificate at the office before you travel, and physically check the expiry date is comfortably past the show's last day.
Better: do not do it. A booth server is a second thing that can fail, in a place with no spare parts, to solve a problem your local SQLite database already solved.
It earns its place in two situations only: a permanent showroom, where it is just a normal server on a normal network; or a booth with ten or more devices syncing large images, where the local network genuinely saves time. For a three-rep booth, the honest answer is that Layer 0 is the booth server, and it fits in your hand.
The hardware kit, itemized
Why tablets beat laptops at a booth
A booth is a standing environment with no desk worth using. A laptop needs a surface, and the surface you have is either a small counter you also need for linesheets, or your forearm.
A laptop screen is also a wall: you stand behind it, the buyer stands in front of it, and it faces away from them. A tablet turns around. Half of booth order writing is showing the buyer the running total and the size breakdown so they can react, and that only works on a device you can hand over.
Beyond the ergonomics, the technical arguments:
- an iPad has a rear camera for scanning sample tags and photographing swatches;
- it has cellular built in without tethering;
- it wakes instantly and does not decide to install an update;
- it can be locked to one app so nobody wanders into Safari;
- it can be tracked and wiped remotely;
- and it charges from the same USB-C power bank as everything else in the kit.
Its battery is also small — Apple lists a "built-in 28.93-watt-hour rechargeable lithium-polymer battery" on the iPad (A16), which sounds like a weakness and is the reason the power math below works out.
The honest counter-argument: typing is slower, and any screen dense enough for back-office work is painful on a tablet. So bring one laptop for the booth manager. It lives in the back of the booth, does reconciliation and reporting, and never faces a buyer.
Which iPad, and how many
Buy the base iPad, cellular, one per rep plus one spare. As of July 2026 the iPad (A16) lists on apple.com at $449 for 128 GB Wi-Fi and $599 for 128 GB Wi-Fi + Cellular; cellular adds $150 at every storage tier (256 GB is $549 and $699, 512 GB is $749 and $899).
128 GB is ample for an ERP client plus a season of product images unless you sync video. Take cellular even if you plan to rely on your own hotspot: $150 buys every device an independent radio and an independent failure domain.
Treat the spare as mandatory. One device will fail. It must be provisioned identically, fully synced and charged, and the same model so cases and cables interchange.
Cases, stands and straps
Convention halls have concrete floors under thin carpet, so a drop a rug would absorb becomes a cracked screen. Specify a rugged case with raised corner bumpers and a hand strap on the back — the strap is what lets a rep hold the device one-handed for nine hours while gesturing with the other. Add a matte screen protector; glossy glass under a truss of LED floods is genuinely hard to read.
Add one counter stand for a "kiosk" iPad that displays the linesheet or a lookbook while nobody is using it, and a neck lanyard or shoulder strap for whichever rep walks the aisles pulling buyers in. Bring a microfiber cloth. Screens get filthy within an hour and a smeared screen makes your software look cheap.
Power, with the math done
Plan in energy. Counting devices tells you nothing about whether you will run out. The arithmetic below covers a three-rep booth on a nine-hour day. Every measured input is labeled; every assumption is labeled too, so you can replace it with your own hardware's numbers.
DEVICE ENERGY BUDGET - 3 reps, 9-hour show day
MEASURED (from the manufacturer's spec page)
iPad (A16) internal battery 28.93 Wh
Apple rating, web on Wi-Fi up to 10 h
Apple rating, web on cellular up to 9 h
ASSUMED (replace with your own observation)
Booth reality: max brightness + camera +
cellular hunting + screen never sleeps ~5.5 h
=> average draw 28.93 Wh / 5.5 h = 5.3 W
=> 9-hour day 5.3 W x 9 h = 47.7 Wh per iPad
=> deficit after internal battery = 18.8 Wh per iPad
=> 3 iPads = 56.3 Wh of deficit
5G hotspot, working hard, 9 h @ ~4.5 W avg ~40 Wh
(assume its internal battery covers 4-6 h)
=> deficit ~20 Wh
Total energy that must come from power banks ~76 Wh
Bank-to-device transfer efficiency (boost
conversion + cable + charge circuit losses) ~70%
=> bank capacity required 76 / 0.70 = 109 Wh
SIZING THE BANKS
mAh -> Wh: mAh / 1000 x cell voltage
A 26,250 mAh bank at 3.8 V nominal = 99.75 Wh
=> 2 banks covers the day with margin
=> +1 bank for the "device died at 2 pm" case
=> +1 bank dedicated to printer/scanner/phones
PACK 4 BANKS. Each is under the 100 Wh carry-on
limit in 49 CFR 175.10, so all 4 fly in the cabin
with no airline approval needed.
Reading it: the key line is the 5.3 W average draw, derived by dividing the real battery capacity by a realistic booth runtime rather than trusting the marketing figure. Apple's "up to 10 hours" assumes moderate brightness and light web use, and its own cellular figure is already an hour lower; a booth iPad runs at full brightness, never sleeps, and hunts for signal, which roughly halves it.
The 70% transfer efficiency is a real, measurable loss — a bank steps cell voltage up to 5, 9, 15 or 20 V and gives off heat doing it, then loses more in the cable and again in the iPad's charging circuit.
The mAh-to-Wh line is the one to memorize, because power banks advertise mAh and aviation rules are written in Wh: divide mAh by 1,000 and multiply by the cell voltage, which is 3.7 or 3.8 V for almost every lithium ion bank.
The output is a purchase decision: four banks, each comfortably under 100 Wh, each with at least one 60 W USB-C PD output. USB-C PD (power delivery) is the standard that lets a charger and a device negotiate a higher voltage than the old 5 V default; without it, a "fast" bank charges an iPad at a trickle. Buy on the printed watt-hour figure on the case. Ignore the mAh in the product name.
Nobody at the booth watches battery percentages, so make it a rule instead: every device that drops below 40% goes on a bank immediately, and stays on it until it reads 90%. Charging while working is normal — the hand-strap case has room for a short cable and the bank sits in a pocket or on the counter. The rule exists because a device at 15% will hit 0% during the one conversation that matters.
The charging bay: order one 500 W / 5 A power drop (Javits advance rate $147.50 for 2026, plus $54.65 for their multi box — some venues require you use theirs, so check). Plug in one multi-port charger of 100 W or more; look for GaN (gallium nitride) in the description, which is the semiconductor that lets a high-wattage charger be the size of a plum instead of a brick.
All four banks live there during the day, rotating out to devices, and everything returns to the hotel at night because show power is switched off after hours unless you bought the 24-hour service.
Label every bank, cable and brick with the device number and your brand in tape; loose electronics walk away, and identical black bricks mix with the neighbouring booth's within a day.
The second-carrier hotspot
Buy a standalone 5G hotspot and put a data-only plan on it from a carrier that is not the carrier your iPads' eSIMs use. The reason is the congestion mechanism from earlier: RRC failures and uplink saturation happen inside one carrier's sector. Two carriers are two independent radio networks, on different spectrum, with different antennas and different backhaul. When one is unusable the other frequently is not.
What a hotspot costs against venue Wi-Fi
Price it yourself, at purchase time, because prepaid data tariffs change every few months and any number printed in a book is wrong by the time you read it. The comparison that does not change is the one against the venue.
Javits charges $262.75 per device in advance for a booth with one to four devices, so four devices costs $1,051.00; at five devices or more the rate drops to $210.00 each, which is still $1,050.00 for five. Either way you are paying four figures for up to five days on a network you do not control. A prepaid data-only plan generous enough for a three-day show — order records, prospect records and product thumbnails, no video — costs a small fraction of one device's venue Wi-Fi fee.
Work out your own expected data volume before you buy. A synced order with a photo or two runs to a few hundred kilobytes, so a booth writing 300 orders moves somewhere between tens and a couple of hundred megabytes across the whole show. Plans are sold in gigabytes, so even a small one has room to spare.
Configuration, done at the office and never at the booth:
- Set a permanent SSID and a strong password, using WPA2 or WPA3, the two current Wi-Fi encryption standards, rather than leaving the network open. Pre-join every device to it at the office. No one should be typing a Wi-Fi password in a hall.
- Disable the 2.4 GHz radio if the hotspot allows it, and run 5 GHz only. 2.4 GHz at a trade show is three channels of noise.
- Set the client limit to exactly the number of devices you own. Attendees will find your network.
- Change the hotspot's admin password from the factory default, and write the new one on the device register rather than on the hotspot.
- Physically place it high — on top of a fixture, or taped to the back of a wall panel above head height. Not on the floor, not inside a case, not under a table. Human bodies absorb 5 GHz well, and a hall is full of them.
- Give it its own power bank, permanently attached. It is the one device nobody will notice has died.
Wired Ethernet, where it exists
iPadOS supports USB-C Ethernet adapters, and most generic USB-C gigabit adapters work with no configuration or driver. Do not take that on trust for your specific adapter: plug it in at the office, turn Wi-Fi and cellular off, and confirm the app syncs. In practice, use a USB-C hub with Ethernet plus power-delivery passthrough, so the iPad charges and has a wire simultaneously from its single port.
Whether to buy a venue drop is a cost question, and the Javits numbers answer it for most brands: no. The exception is a booth running card payments through a terminal that needs a wire, where the dedicated credit-card drop ($346 advance at Javits 2026) is far cheaper than a general internet line.
If your venue sells a basic wired drop for a few hundred rather than a few thousand, this varies enormously by city, take it, plug it into a travel router, broadcast your own SSID from that router, and you have Layer 3.
Do not plug iPads straight into a venue drop, and never put the printer on venue Wi-Fi, because client isolation will stop the iPads reaching it.
Printing at the booth
Some buyers want paper before they walk away, and the ones who want it tend to be the ones who matter. A mobile thermal printer solves this without ink, toner or a mains cable — thermal meaning it burns the image into heat-sensitive paper instead of putting anything on top of it.
The Brother PocketJet PJ-883 is a reasonable reference part: 8.5-inch print width, 300 dpi, direct thermal so paper is the only consumable, AirPrint and MFi support plus Bluetooth 5.2 and Wi-Fi, 600 pages per battery charge, and 1.34 lb (610 g) with the battery fitted.
AirPrint is Apple's built-in printing system, so an iPad can print with no driver; MFi ("Made for iPhone/iPad") is Apple's accessory certification, which is what allows an app to talk to the hardware directly. Brother lists it at $612.00 as of July 2026.
There are two caveats. Thermal paper fades with heat and light, so the printed sheet is a courtesy copy for the buyer's bag and never your archival record — that is the row in your database and the PDF you email later.
And AirPrint discovery uses multicast, a way of sending one packet to every device on a local network at once, which is what venue guest Wi-Fi blocks in order to isolate clients from each other. So the printer joins your hotspot's SSID, or pairs over Bluetooth to one designated iPad. Decide which before you travel and test it.
Scanning
Two options, suiting different volumes. Camera-based scanning through iOS's VisionKit DataScannerViewController (iOS 16 and later) costs nothing, is built into the OS, and reads barcodes and text through the rear camera. Right for occasional use: a rep points the iPad at a sample's hangtag to jump straight to that style instead of scrolling. Check DataScannerViewController.isSupported and .isAvailable before you present it, and design a manual-entry fallback for when either is false — camera permission can be declined, and the camera can be busy.
A dedicated Bluetooth scanner earns its place when someone scans hundreds of tags — counting the sample rail at load-out, for example. The Socket Mobile S720 is a reasonable reference part. It reads 1D codes (the striped ones) and 2D codes (the square ones: QR, Data Matrix, PDF417, Aztec), connects over Bluetooth, and Socket rates its battery at over 20 hours of standby, or up to 70,000 scans within 9 hours at two scans a second (socketmobile.com, checked July 2026).
Apps talk to it through Socket's CaptureSDK. Socket publishes no list price on the product page, so budget from a reseller quote. The ergonomic argument is the real one: it works one-handed while the other hand holds the garment, and it does not commandeer the iPad's screen.
One thing not to assume: buyer badges. Many shows encode registration data in the badge, but the encoding belongs to the organizer and is usually readable only through their lead-retrieval app or a rented scanner. Ask what the format is and whether they offer an API or a post-show CSV export. Never build against a badge format you have not seen.
Paper backup forms
Print them. Bring them. Never use them, but bring them. Two-part carbonless (NCR) forms, pre-printed with your style numbers, colorways and size runs for the season, one page per delivery window, in a clipboard in the back of the booth. The rule attached to them, which goes on the cheat sheet: if you write on paper, photograph the page with your phone before the buyer walks away, and key it in that evening with the photo attached to the order record. Paper that is not photographed immediately becomes paper that is lost in a tote bag.
Packing checklist
| Item | Qty for a 3-rep booth | Why it is on the list | Notes |
|---|---|---|---|
| iPad (A16), Wi-Fi + Cellular, 128 GB | 4 (3 + 1 spare) | Order writing, one per rep | eSIM only; activate plans at the office |
| Rugged case with hand strap | 4 | Concrete floors under thin carpet | Raised corners; strap on the back |
| Matte screen protector | 4 fitted + 2 spare | Show lighting is brutal on glossy glass | Fit before you travel |
| Counter stand for iPad | 1 | Kiosk mode: lookbook / linesheet | Weighted base, not a folding easel |
| Power bank, 90–100 Wh, 60 W+ USB-C PD | 4 | Covers a 9-hour day with margin | Under 100 Wh so it flies carry-on |
| Multi-port GaN charger, 100 W+ | 2 | Recharges banks during the day | One at booth, one at hotel |
| USB-C cable, 1 m, 100 W rated | 10 | They vanish | Label every one |
| USB-C cable, 2 m | 4 | Reach from counter to bank | |
| Standalone 5G hotspot, carrier B | 1 | Independent radio failure domain | Different carrier from the iPads |
| USB-C hub with Ethernet + PD passthrough | 2 | Wired fallback; charge while wired | Test your exact adapter before travel |
| Travel router | 1 | Turns a venue drop into your own SSID | Only if you bought a wired drop |
| Ethernet cable, 25 ft | 1 | Venue drops land at the back wall | Flat cable tapes down better |
| Mobile thermal printer + spare battery | 1 | Buyer wants paper now | Joins your SSID, not the venue's |
| Thermal paper packs | 3 | ~600 pages per battery charge | Direct thermal: no ink to forget |
| Bluetooth 1D/2D scanner | 1 | Sample rail counts at load-out | Optional if volume is low |
| Booth manager laptop | 1 | Reconciliation, reporting, back office | Never faces a buyer |
| Pre-printed 2-part paper order forms | 50 sets | Total-failure fallback | Photograph every page immediately |
| Clipboard, pens, permanent marker | 2 / 10 / 2 | Paper needs a surface | |
| Label tape / gaffer tape | 1 roll each | Labeling kit; taping cables down | Gaffer, not duct — leaves no residue |
| Printed device register | 2 copies | Serial number, assigned rep, and the IMEI (the radio's unique id) | Needed to report a theft |
| Printed one-page cheat sheet | 1 per rep + 2 spare | Nobody reads a wiki at a booth | Laminate it |
| Printed escalation card | 2 | Who to call, in order, with numbers | Include the venue tech desk number |
| Microfiber cloth | 4 | Smeared screens look cheap | |
| Small padlock / cable lock | 1 | Overnight for anything left | Better: leave nothing |
The pre-show runbook, counted in days
Most of the risk lands before the doors open. The two weeks beforehand are when every decision gets made that you cannot change later. Work backwards from the first buyer appointment.
| When | Do | Done when |
|---|---|---|
| T−21 | Order venue services at the advance rate: power, and the credit-card drop if you need it. Confirm booth number, move-in time, and whether exhibitor hotspots are permitted. | Order confirmation PDF filed; the organizer's answer on hotspots is in writing. |
| T−14 | Catalog freeze. No new styles, no renamed colorways, no changed size runs after today. | Freeze flag set in the database; merch and design have both acknowledged in writing. |
| T−14 | Price list verification: every style in the show assortment has a wholesale price, a currency, and a valid date range for this season. | The verification queries below return zero rows. |
| T−14 | Device provisioning: enroll every iPad through Automated Device Enrollment, push the app, the Wi-Fi profile for your hotspot, and single-app restrictions. | All four devices appear as supervised and compliant in the MDM console. |
| T−12 | Full dataset sync on every device, on the office network, watched to completion. | Local row counts match the server for catalog, price list and customers. |
| T−10 | Offline rehearsal. Wi-Fi off, cellular off, write ten orders end-to-end on each device including a new prospect and an out-of-catalog request. | Every rep has personally done it on their own device, with you watching. |
| T−7 | Sync the rehearsal orders back and reconcile them, then void the test data with a reason code so it cannot contaminate the show. | Test orders visible on the server, then voided; nothing hard-deleted; audit trail intact. |
| T−7 | Activate the hotspot plan and every iPad eSIM plan. Confirm data flows on each. | Speed test passed on every SIM at the office. |
| T−7 | Every rep signs in on their own device, on the office network. Nobody signs out again until the order book is reconciled. | Rep initials the device register next to their device. |
| T−7 | Check Authentication → Sessions: time-box and inactivity timeout are off, or set well beyond the show length. | Screenshot of the settings filed with the show log. |
| T−7 | Confirm the oversell resolution policy in writing, and who has authority to apply it. | Policy is one paragraph, printed on the cheat sheet. |
| T−7 | Turn on point-in-time recovery on the production database if it is not already on, and confirm the retention window covers the show plus a week. | PITR shows an active window in the Supabase dashboard. |
| T−3 | Freeze production deploys. No schema changes, no app releases, no dependency bumps until the show closes. | Branch protection or a written rule the whole team acknowledged. |
| T−3 | Print: paper order forms, device register, cheat sheets, escalation card, linesheets. | Physically in the case. |
| T−1 | Final sync on every device, on the office network, verified by row count and by eye. | Readiness script prints ALL CHECKS PASSED on all four devices. |
| T−1 | Charge everything to 100%: iPads, banks, hotspot, printer, scanner. Print one test page. | Every item logged as charged on the device register. |
| T−1 | Each rep opens their own device and confirms the season shown on the home screen is the season being sold. | Rep initials the register next to their device. |
| Morning of | Arrive at move-in, place the hotspot high, join every device, run the morning check, refresh the ATS snapshot. | Snapshot timestamp on every device is from this morning. |
Catalog freeze and price list verification
Catalog freeze exists because a style that changes after devices have synced produces two different truths in one booth. Rep A's iPad says the jacket comes in four colors; Rep B's, synced a day later, says five. Both sell confidently, and one is wrong in front of a customer. Freeze, then verify, then sync, in that order.
The queries below run in psql, the command-line client that ships with Postgres. The \set lines create a variable you can reuse, and :'season' pastes it back in with quotes around it. One more term: landed cost is what a unit actually costs you by the time it reaches your warehouse — factory price plus freight, duty and handling, and chapter G builds it. Selling below it means losing money on every unit you ship.
-- Pre-show data integrity. Queries 1-4 must return ZERO rows.
-- Run at T-14, then again at T-1. Any row is a blocker.
-- Queries 1-4 only READ. Step 5 WRITES; read its note first.
\set season 'SS27'
-- 1. Show styles with no active wholesale price.
SELECT 'no_price' AS problem, v.sku, s.style_code, s.name
FROM variants v
JOIN styles s ON s.id = v.style_id
JOIN show_assortment a ON a.style_id = s.id
LEFT JOIN price_list_items p
ON p.variant_id = v.id
AND p.price_list = 'WHOLESALE_USD'
AND daterange(p.valid_from, p.valid_to) @> CURRENT_DATE
WHERE a.season = :'season'
AND p.id IS NULL;
-- 2. Prices that look like data entry errors.
SELECT 'suspect_price', v.sku, p.unit_price, s.landed_cost
FROM price_list_items p
JOIN variants v ON v.id = p.variant_id
JOIN styles s ON s.id = v.style_id
JOIN show_assortment a ON a.style_id = s.id
WHERE a.season = :'season'
AND p.price_list = 'WHOLESALE_USD'
AND (p.unit_price <= s.landed_cost -- selling below cost
OR p.unit_price > s.landed_cost * 8); -- fat finger
-- 3. Styles in the assortment with no sellable variants.
SELECT 'no_variants', s.style_code, s.name
FROM styles s
JOIN show_assortment a ON a.style_id = s.id
WHERE a.season = :'season'
AND NOT EXISTS (
SELECT 1 FROM variants v
WHERE v.style_id = s.id AND v.active
);
-- 4. Missing delivery windows: a rep cannot write a ship date
-- the system does not know about.
SELECT 'no_delivery_window', s.style_code
FROM styles s
JOIN show_assortment a ON a.style_id = s.id
WHERE a.season = :'season'
AND (s.ship_start IS NULL OR s.ship_cancel IS NULL
OR s.ship_cancel <= s.ship_start);
-- 5. Freeze the assortment. THIS ONE WRITES.
-- Run it only after 1-4 all returned zero rows. It is wrapped
-- in a transaction so that if the reported row count is not
-- the number of styles you expect, you type ROLLBACK instead
-- of COMMIT and nothing has happened.
BEGIN;
UPDATE show_assortment
SET frozen_at = now(), frozen_by = current_user::text
WHERE season = :'season' AND frozen_at IS NULL;
-- psql prints "UPDATE n" above. Is n the style count you expect?
-- yes -> COMMIT; no -> ROLLBACK;
COMMIT;
Each query hunts one class of problem that only becomes visible at the booth:
- Query 1 catches a style a rep can show but cannot price, forcing them to invent a number.
- Query 2 catches prices below landed cost or absurdly above it — the two shapes a fat-finger error takes.
- Query 3 catches a style with no active sizes or colors, which appears in the catalog and then cannot be added to an order.
- Query 4 catches missing or inverted ship windows, which produce orders no factory can plan against.
The final statement stamps the freeze so later changes show up as violations rather than silent drift; it is the only statement here that changes data, which is why it sits inside BEGIN and COMMIT with a checkpoint where you read the row count before committing.
Expected output: four empty result sets, then UPDATE n, then COMMIT. If any of the first four returns rows, go and fix the data. Loosening the query so the rows disappear fixes nothing and hides the problem until the booth.
What you are protecting: backups, RPO and RTO
Two numbers describe every backup plan, and you should be able to say both out loud before a show. RPO (recovery point objective) is how much data you are willing to lose, measured in time: an RPO of five minutes means that after a disaster you accept losing up to five minutes of work. RTO (recovery time objective) is how long you are willing to be unable to work while you recover.
They are separate, and people confuse them constantly: a nightly backup gives you a 24-hour RPO no matter how fast you can restore it.
PITR (point-in-time recovery) is the feature that makes RPO small. Instead of restoring only to last night's snapshot, it continuously archives the database's write-ahead log, the ordered record of every change, so you can restore to any moment inside a retention window, including the second before someone ran the wrong UPDATE. Turn it on before the show and confirm the retention window covers the show plus a week, because show damage is often discovered days later during reconciliation.
An unsynced order lives on one iPad and nowhere else
The part specific to a trade show inverts the usual thinking. During show hours, an order that has not synced yet lives on the iPad and nowhere else, which makes that iPad the database of record for it.
An order written at 11:20 and still queued at 16:00 exists in exactly one place on earth: a SQLite file on one iPad. No server backup contains it. That produces one hard rule, and it is the most expensive rule in this chapter to learn the other way.
Never wipe, reset, factory-restore or reinstall a device whose upload queue has not drained to zero. Not to "fix" a sync problem, not on a support call, not because MDM offers a tidy button labeled Erase. Get the queue to zero first, confirm the count on screen, and only then touch the device. The one exception is a device you believe has been stolen, where the security loss outweighs the data loss, and even then, write down what that device was holding before you send the wipe.
The device readiness script
Run this on the booth manager's laptop at T−1 against each device's exported diagnostic, or adapt it to run inside the app as a hidden readiness screen. The point is a single pass/fail that a non-engineer can read. It only reads; it changes nothing. Two tools appear in it: curl, which fetches a web address from the command line, and jq, which pulls a named value out of a JSON response.
#!/usr/bin/env bash
# preshow-check.sh - run once per device at T-1. Read-only.
# Usage:
# export ADMIN_TOKEN=... # never hard-code it in here
# export RELEASE_BUILD=2026.7.3
# ./preshow-check.sh DEVICE-03 SS27
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "usage: $0 <device-label> <season>" >&2
exit 2
fi
: "${ADMIN_TOKEN:?export ADMIN_TOKEN before running}"
: "${RELEASE_BUILD:?export RELEASE_BUILD before running}"
DEVICE="$1"; SEASON="$2"
API="https://api.yourbrand.com"
FAIL=0
# Keep the token OFF the command line. Anyone with an account on
# this machine can read another process's arguments with `ps`, so
# `curl -H "Authorization: Bearer $TOKEN"` leaks it. Instead curl
# reads the header from a 0600 file that is deleted on exit,
# however the script exits.
CFG="$(mktemp)"
chmod 600 "$CFG"
trap 'rm -f "$CFG"' EXIT
printf 'header = "Authorization: Bearer %s"\n' "$ADMIN_TOKEN" >"$CFG"
api() { curl -sS --fail --max-time 10 --config "$CFG" "$API$1"; }
say() { printf '%-34s %s\n' "$1" "$2"; }
check(){ # check "label" actual expected
if [ "$2" = "$3" ]; then say "$1" "OK ($2)"
else say "$1" "FAIL (got $2, want $3)"; FAIL=1; fi
}
num(){ # num "label" value -lt|-ge limit
case "$2" in ''|*[!0-9]*)
say "$1" "FAIL (not a number: '$2')"; FAIL=1; return;;
esac
if [ "$2" "$3" "$4" ]; then say "$1" "OK ($2)"
else say "$1" "FAIL ($2, wanted $3 $4)"; FAIL=1; fi
}
echo "=== Pre-show check: $DEVICE / $SEASON ==="
if ! D=$(api "/admin/devices/$DEVICE/diagnostic"); then
echo "BLOCKED - cannot read diagnostic for $DEVICE." >&2
echo "Check network, token and device label." >&2
exit 1
fi
check "App build matches release" \
"$(jq -r '.app_build // "missing"' <<<"$D")" "$RELEASE_BUILD"
check "Season on home screen" \
"$(jq -r '.active_season // "missing"' <<<"$D")" "$SEASON"
check "Catalog freeze acknowledged" \
"$(jq -r '.catalog_frozen // "missing"' <<<"$D")" "true"
check "Upload queue empty" \
"$(jq -r '.upload_queue // "missing"' <<<"$D")" "0"
check "Assigned rep set" \
"$(jq -r '.rep_id != null' <<<"$D")" "true"
check "Signed in (refresh token present)" \
"$(jq -r '.has_refresh_token // false' <<<"$D")" "true"
# Row-count parity: local SQLite vs server.
for T in styles variants price_list_items customers; do
L=$(jq -r ".local_counts.$T // \"missing\"" <<<"$D")
S=$(api "/admin/counts/$T?season=$SEASON" | jq -r '.count')
check "Row parity: $T" "$L" "$S"
done
num "Last sync age (minutes)" \
"$(jq -r '.last_sync_age_minutes // ""' <<<"$D")" -lt 120
num "Battery percent" \
"$(jq -r '.battery_percent // ""' <<<"$D")" -ge 95
echo
if [ "$FAIL" -eq 0 ]; then
echo "ALL CHECKS PASSED - $DEVICE is show-ready."
else
echo "BLOCKED - fix every FAIL line above before travel."
exit 1
fi
What it does: pulls one diagnostic blob per device from an admin endpoint, then asserts twelve things a human would otherwise have to remember — six about the device's state, four row counts, and two numeric thresholds.
The build check stops a device running last month's app. The season check has embarrassed the most people — a device still pointed at the previous season shows the wrong catalog and the wrong prices while looking completely normal.
The upload-queue check stops leftover test orders traveling with you, and the refresh-token check confirms the rep is actually signed in, which is the failure you cannot fix in a hall. The row-parity loop compares local SQLite counts against the server for the four tables a rep depends on, catching a partial sync that finished without erroring.
Three details in the script are there for safety rather than for the checks themselves:
- The admin token is read from the environment and then written into a curl config file with mode 0600, file permissions that let only the owner read it, because arguments passed to
curlare visible to every other user on the machine throughps; thetrapdeletes that file however the script exits, including on error. set -euo pipefailplus the:?checks mean a missing token or a missing argument stops the script with a sentence instead of a confusing failure twenty lines later.- And the
numhelper rejects non-numeric input explicitly, because a diagnostic field that arrives asnullwould otherwise crash the comparison and look like a network fault.
Expected output is a column of OK lines and ALL CHECKS PASSED; a non-zero exit means that device does not travel until someone fixes it.
Nothing ships to production from T−3 until the show closes and the order book is reconciled. Not a schema migration, not a "tiny" copy fix, not a dependency bump. A show is the one time you have no ability to roll back gracefully, because the people affected are standing in a hall with customers. Put the freeze in writing and give one named person the authority to break it.
Running the show
The order-writing flow at the booth
Design the flow so the fastest path through the software is also the correct one. Where the correct path is slower than the shortcut, reps take the shortcut every time, and they are right to.
- Identify the account. Search by name; results include existing customers and prospects created earlier at this show. Every account shows a credit chip — a small colored label on screen, the size of a word — reading approved with a limit, prepay only, pending review, or unverified. The rep learns the terms situation before the conversation starts.
- Open an order. Season, delivery window (ship start and cancel date), currency, price list and terms all default from the account and the show. Defaults matter: every field a rep must fill is a field they will fill wrong when busy.
- Walk the line. The rep taps a style, picks a colorway, and enters quantities against a size grid — a row of boxes, one per size — or a prepack. Size grids beat one line per SKU because a buyer says "six smalls, ten mediums", not "six of SKU 4471-OLV-S".
- Watch the running total. Units, gross value, discount, net value and minimum-order status, all visible and updating. Turn the screen to the buyer at least twice.
- Capture the exceptions. Split deliveries, exclusivity requests, labeling and packing requirements (chapter I), a promised sample. Free-text notes on the order or the line, capturable in two taps — otherwise they get written on a hand.
- Review and close. Read the order back, signature on glass, email or print a copy, mark it submitted.
The status model behind that flow is what makes the offline design visible to users. Name the states honestly and show them.
// Order states the rep can see. Nothing is hidden.
export type OrderStatus =
| 'draft' // being written right now, on this iPad
| 'submitted' // rep finished; buyer has left; local only
| 'queued' // in the upload queue, waiting for network
| 'pending' // uploaded; server has not yet decided
| 'confirmed' // server accepted; stock allocated
| 'needs_review' // accepted, but a line could not be allocated
| 'rejected'; // server refused (bad price list, closed acct)
// The ONLY transitions the device may perform by itself.
const LOCAL_TRANSITIONS: Record<string, OrderStatus[]> = {
draft: ['submitted'],
submitted: ['queued'],
queued: ['submitted'], // rep reopened it before upload
};
// Everything below is set by the server and arrives via sync.
// The device NEVER writes these locally.
const SERVER_OWNED: OrderStatus[] =
['pending', 'confirmed', 'needs_review', 'rejected'];
export function canEditLocally(s: OrderStatus): boolean {
return s === 'draft' || s === 'submitted' || s === 'queued';
}
// Chip copy that a stressed human reads correctly at a glance.
export const CHIP: Record<OrderStatus, { text: string; tone: string }> = {
draft: { text: 'Writing', tone: 'neutral' },
submitted: { text: 'Saved on this iPad', tone: 'neutral' },
queued: { text: 'Waiting to send', tone: 'neutral' },
pending: { text: 'Sent — confirming', tone: 'info' },
confirmed: { text: 'Confirmed', tone: 'good' },
needs_review: { text: 'Office is checking', tone: 'warn' },
rejected: { text: 'Needs attention', tone: 'bad' },
};
Three things this encodes. "Saved on this iPad" is a real, named, visible state, so a rep can point at it and truthfully tell a buyer the order is recorded.
The device is structurally incapable of marking an order confirmed, because confirmed only ever arrives through sync — the create-not-mutate rule expressed as types, with SERVER_OWNED as the list your sync layer asserts against.
And the chip text avoids engineering words: "Office is checking" is what needs_review means to a rep and to the buyer beside them, while "conflict detected in allocation" belongs in the back office.
Saving an order must complete in local storage and return control instantly, always, with no network involved. If your submit button waits on an upload you have built a machine that fails hardest at the exact moment it matters most. The rep taps Submit, the order is saved, the chip says "Waiting to send", and the buyer walks away happy. Uploading is a background concern that resolves in its own time.
When the buyer asks for something not in the catalog
This happens several times a day and is the most common route to bad data. Four different situations sit behind it, and they need different handling:
- It exists but is not in the show assortment. A style from the carryover range — styles you keep selling season after season instead of retiring — or one you held back. Give the rep a "show everything" toggle that reveals non-assortment styles, clearly marked, with prices from the same verified price list.
- It exists and the device is stale. A late addition that missed the freeze. This is what the freeze prevents, and if it happens anyway the fix is a sync, not a workaround.
- It does not exist and might. A custom colorway, a fabric substitution, an exclusive make-up for that retailer — trade shorthand for a version of a style produced for one customer only. This is a real commercial conversation and it must be recorded.
- It does not exist and will not. A discontinued style. Record the request as demand signal, then move on.
For the last two the rule is absolute: the rep never creates a SKU on the iPad. A style number invented at a booth propagates into production planning, the item master (the single official list of everything you sell) and a factory purchase order, and takes months to extract. Instead the rep creates a request line: free text, a photo, an indicative quantity and price, and a flag that keeps it out of every downstream calculation until merchandising approves it.
-- Request lines: demand captured without polluting the item master.
CREATE TABLE order_request_lines (
id uuid PRIMARY KEY, -- generated on device
order_id uuid NOT NULL REFERENCES orders(id),
created_by uuid NOT NULL REFERENCES users(id),
created_at timestamptz NOT NULL DEFAULT now(),
-- What the buyer actually asked for, in their words.
description text NOT NULL,
based_on_style uuid REFERENCES styles(id), -- nullable
photo_path text, -- device-local, synced later
-- Commercially indicative ONLY. Never priced, never costed,
-- never allocated, never sent to a factory in this state.
indicative_qty integer CHECK (indicative_qty > 0),
indicative_price numeric(12,2) CHECK (indicative_price >= 0),
status text NOT NULL DEFAULT 'requested'
CHECK (status IN ('requested','approved',
'declined','converted')),
resolved_by uuid REFERENCES users(id),
resolved_at timestamptz,
converted_line uuid REFERENCES order_lines(id)
);
-- A request line can never become a commitment by accident.
CREATE OR REPLACE FUNCTION guard_request_conversion()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.status = 'converted' AND NEW.converted_line IS NULL THEN
RAISE EXCEPTION
'request line % marked converted with no order line', NEW.id;
END IF;
IF NEW.status <> OLD.status AND NEW.resolved_by IS NULL THEN
RAISE EXCEPTION
'request line % changed status with no approver', NEW.id;
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER trg_guard_request_conversion
BEFORE UPDATE ON order_request_lines
FOR EACH ROW EXECUTE FUNCTION guard_request_conversion();
-- Order totals must exclude requests. Belt and braces.
CREATE VIEW order_committed_value AS
SELECT o.id,
COALESCE(SUM(l.quantity * l.unit_price), 0) AS committed_value
FROM orders o
LEFT JOIN order_lines l ON l.order_id = o.id
GROUP BY o.id;
The table gives a rep somewhere honest to put "she wants this in olive", with a photo taken on the spot and an indicative quantity so merchandising can see whether three buyers asked for the same thing.
The trigger enforces two rules a database can check and a human cannot: nothing reaches converted without pointing at a real order line, and no status changes without a named approver.
The view keeps every total in the system built from committed lines only — note the LEFT JOIN, so an order with no lines yet reports zero rather than vanishing from reports, and because the view never looks at order_request_lines, a request can never quietly inflate an order's value or the season's booking number — the total value of orders taken, which is the figure everyone upstairs watches.
Expected behavior: UPDATE order_request_lines SET status='converted' without a line raises an exception and the whole statement rolls back.
New accounts and credit approval status
A meaningful share of a show's value is meeting retailers you have never sold to. The instinct is to type them straight in as customers. Do not: a customer record carries credit terms, a credit limit, tax treatment and a payment history, and none of those can be established at a booth.
Field devices create prospects. A prospect holds everything a rep can honestly collect:
- legal entity name, trading name, ship-to and bill-to addresses;
- buyer's name, email and mobile;
- store count, website or social handle;
- the tax registration number read off their phone;
- whether they hold a resale certificate (the document that lets a retailer buy from you without paying sales tax, because their customer pays it instead);
- requested payment terms, and how they heard about you.
That is a complete lead, and it stops short of being an account.
Credit status is then a small, visible state machine — a fixed list of states a record can be in, with defined moves between them. Chapter K covers the finance behind it. The booth needs four states and one rule. Two words in the table: a proforma invoice is a bill sent before you ship, so the buyer can pay in advance; the open limit is how much of an approved credit limit is still unused once you subtract what the account already owes.
| Credit state | What the chip says | Rep may write an order? | What happens after the show |
|---|---|---|---|
unverified | New account | Yes | Credit application sent; nothing ships until it clears |
pending_review | Credit in review | Yes | References and credit agency checked; decision within the review SLA |
prepay_only | Prepay before ship | Yes | Proforma invoice issued; ship on cleared funds |
approved_limit | Approved — $X open | Yes | Ships on terms, subject to the open limit at ship date |
The rule: credit never blocks writing an order; credit blocks shipping one. A rep blocked from writing an order because a credit check has not run loses a sale a phone call could have saved. A warehouse that ships to an unapproved account loses the whole invoice.
Put the constraint where the money moves, and keep the chip visible so the rep sets expectations honestly: "I can take this now; our credit team will come back within five working days, and if terms aren't in place before delivery we'd need it prepaid."
Taking deposits
Deposits reduce the cancellation rate on new and unproven accounts and are worth taking. The mechanics deserve care, because taking card payments on a broken network shifts real financial risk onto you.
What Square and Stripe promise offline
Square's offline mode is explicit about it. You set a maximum per-transaction amount for offline payments — Square's help documentation describes it as "between $1 and $50,000", and pending offline payments "will expire after 72 hours if you don't reconnect to the internet and upload them".
Read the liability sentence to your finance person: "You're responsible for any expired, declined, or disputed payments accepted while taking offline payments." Square also states that it "is unable to provide customer contact information or contact customers on your behalf for declined or expired offline payments", so a failed charge is simply a loss with no one to chase.
Several payment types do not work offline at all, including Afterpay, Cash App Pay, Square gift cards, Tap to Pay on iPhone, Tap to Pay on Android and manually-entered cards (Square help center, checked July 2026).
Stripe Terminal offers a comparable mode. Stripe documents that the reader or SDK stores the payment locally, survives a reboot with those payments intact, and that "when a network connection is restored, the SDK or smart reader automatically forwards any stored payments to Stripe".
Offline support covers the major card brands by chip or contactless tap — Visa, Mastercard, American Express, Discover, China UnionPay, JCB, Maestro, eftpos and Cartes Bancaires — plus NFC mobile wallets such as Apple Pay. Swiping is not allowed, cardholders in the European Economic Area must insert the card and enter a PIN, and several regional debit networks (NYCE, PULSE, STAR, girocard, Interac) are excluded.
On internet readers the supported hardware is the Stripe Reader S700/S710 and the BBPOS WisePOS E (Stripe docs, checked July 2026).
Both vendors carry the same structural risk, and switching vendors will not remove it. A card that would have been declined online is still declined when the stored payment is forwarded, except that by then the customer has gone. You accepted an unverified promise, so you carry the loss.
An offline card payment is an unverified promise you have agreed to accept liability for. If it declines on upload you have neither the money nor a way to contact the cardholder. For a wholesale deposit, capture consent and charge online: tokenize the card onto the account, store a reference issued by your payment provider instead of the card number itself, get written authorization for the deposit amount on the signed order, and run the charge that evening from the hotel. If the buyer wants something at the booth, print the signed order acknowledging the authorization rather than a payment receipt.
If you do accept offline cards, set three controls first:
- a per-transaction ceiling well below your typical order value,
- a rule that offline payments upload before the team leaves the hall each day,
- and a named person who checks next morning that every one settled.
And keep card handling off the venue guest network entirely — Javits, for one, states in its own catalog that "standard Wi-Fi isn't secure enough for credit card processing" and sells a separately configured service for it.
The discipline of the read-back
This is the highest-value habit in the playbook and it costs 90 seconds. Before the buyer leaves, the rep turns the iPad around and reads the order back: total units, total value, ship window, terms, and every exception note. The buyer confirms, signs on glass, and gets a copy — printed, emailed, or both.
It works because of when it happens. Every disagreement you would otherwise have four weeks later — quantities, colors, delivery dates, a discount someone remembers differently — gets resolved here, for free, while both people are standing in front of the samples. It also catches your own data-entry errors at the only moment they are cheap: a 300 that should be a 30 is obvious read aloud and invisible in a column of numbers.
Every order is read back to the buyer, confirmed, and signed before they walk away. Build it into the software so it cannot be skipped: the order cannot move from draft to submitted without a captured signature and a checkbox confirming the totals were read back. Ninety seconds at the booth prevents a dispute that takes hours, and prevents the specific disaster of a buyer who believes they ordered something different from what your factory is now making.
Sync and reconciliation after each day
The evening ritual separates a show that produces a clean order book from one that produces six weeks of archaeology. Do it every night, in the same order, with the same checklist, including the night everyone is tired and the numbers look fine.
The evening sequence
- Get every device onto a real network. Hotel Wi-Fi, or the hotspot, or a rep's home connection. Every device, including the spare, including any device that was not used today.
- Watch every upload queue drain to zero. Not "start syncing" — drain to zero, confirmed on screen, device by device. Any device that will not drain is tonight's problem, not tomorrow's.
- Run the reconciliation queries below. The booth manager runs them, reads the numbers out loud, and writes them in the show log.
- Work the review queue to empty. Every oversell, every rejected order, every duplicate suspect gets a decision tonight.
- Compare against the paper. Any order written on paper today gets keyed in tonight, with the photograph attached.
- Refresh the ATS snapshot for tomorrow. Devices should start the next day knowing what is actually left.
- Charge everything. All devices, all banks, hotspot, printer, scanner.
-- Evening reconciliation. Run in this order, every night.
-- Every statement here READS ONLY. Nothing is modified.
-- Read the numbers out loud. Write them in the show log.
\set show_id 'coterie-ss27'
\set today '2026-09-09' -- or use CURRENT_DATE
-- 1. Did every device report in? Silence is the danger signal.
SELECT d.device_label,
d.assigned_rep,
COUNT(o.id) AS orders_today,
MAX(o.synced_at) AS last_upload,
now() - MAX(o.synced_at) AS silent_for
FROM devices d
LEFT JOIN orders o
ON o.device_id = d.id
AND o.show_id = :'show_id'
AND o.written_at::date = :'today'::date
WHERE d.show_id = :'show_id'
GROUP BY d.device_label, d.assigned_rep
ORDER BY last_upload NULLS FIRST;
-- 2. Anything still not decided by the server.
SELECT o.status, COUNT(*), SUM(v.committed_value) AS value
FROM orders o
JOIN order_committed_value v ON v.id = o.id
WHERE o.show_id = :'show_id'
GROUP BY o.status
ORDER BY o.status;
-- 3. Open oversell tickets - the queue that must reach zero.
SELECT r.id, r.sku, r.requested_qty, r.available_qty,
c.name AS customer, o.written_at, u.name AS rep
FROM oversell_review r
JOIN orders o ON o.id = r.order_id
JOIN customers c ON c.id = o.customer_id
JOIN users u ON u.id = o.written_by
WHERE r.resolved_at IS NULL
AND o.show_id = :'show_id'
ORDER BY o.written_at;
-- 4. Duplicate suspects: same account, same value, minutes apart.
SELECT a.id AS order_a, b.id AS order_b, c.name,
a.written_at, b.written_at,
b.written_at - a.written_at AS gap,
va.committed_value AS value
FROM orders a
JOIN orders b
ON b.customer_id = a.customer_id
AND b.id > a.id
AND b.written_at BETWEEN a.written_at
AND a.written_at + interval '20 minutes'
JOIN order_committed_value va ON va.id = a.id
JOIN order_committed_value vb ON vb.id = b.id
JOIN customers c ON c.id = a.customer_id
WHERE a.show_id = :'show_id'
AND a.status <> 'voided'
AND b.status <> 'voided'
AND va.committed_value = vb.committed_value
AND va.committed_value > 0;
-- 5. Orders with no signature, or read-back not confirmed.
SELECT o.id, c.name, u.name AS rep, o.written_at
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN users u ON u.id = o.written_by
WHERE o.show_id = :'show_id'
AND (o.signature_path IS NULL OR NOT o.readback_confirmed);
-- 6. The number the founder actually wants.
-- Count orders and units SEPARATELY. Joining orders to
-- order_lines and then summing a per-order total multiplies
-- that total by the number of lines on the order. The result
-- looks plausible, which is exactly what makes it dangerous.
SELECT COUNT(DISTINCT o.customer_id) AS accounts_written,
COUNT(*) AS orders,
SUM(v.committed_value) AS booked_value,
(SELECT COALESCE(SUM(l.quantity), 0)
FROM order_lines l
JOIN orders o2 ON o2.id = l.order_id
WHERE o2.show_id = :'show_id'
AND o2.status NOT IN ('rejected','voided')) AS units
FROM orders o
JOIN order_committed_value v ON v.id = o.id
WHERE o.show_id = :'show_id'
AND o.status NOT IN ('rejected','voided');
Query 1 is the most important and the least obvious. It lists every device, including ones that uploaded nothing, sorted so silence appears first. A device with zero orders is either a rep who had a quiet day or a device that has not synced for two days and is holding a day's work — identical from the booth, so you go and physically check.
Query 2 tells you whether the server has finished deciding; a growing pending count means the queue worker is behind. Query 3 is the queue that must reach zero tonight.
Query 4 uses a deliberately loose heuristic — same account, same value, within twenty minutes, because the common show duplicate is a rep tapping Submit twice, and a loose filter a human reviews beats a strict one that misses cases. Query 5 catches skipped read-backs while the rep still remembers the conversation.
Query 6 is the number everyone asks for at dinner, and it is written the long way on purpose. The obvious version joins orders to order lines and sums everything in one pass, which counts each order once per line and inflates both the order count and the booked value — a five-line order counts five times. Because the inflated figure is merely large rather than absurd, nobody notices until someone else computes it differently.
Counting orders in the outer query and units in a subquery keeps each number at its own grain, and excluding rejected and voided orders stops dead paperwork inflating the season. Compute it the same way every night and three people stop producing three different totals.
Resolving oversell as a business decision
Two reps sold the same 300 units. The system did the correct thing: it accepted both orders as facts and refused to guess. Now a human decides, and what they are deciding is a commercial question about which customer relationship matters more.
A timestamp is the worst available tiebreaker and the one systems default to. Whose order reached the server first depends on which rep happened to walk near the hotspot, which is a coin flip dressed up as a rule. It also produces a conversation you cannot defend: telling your largest account they lost units to a first-time boutique because of network timing.
So write the policy before the show, in one paragraph, and put it on the cheat sheet. A workable default for a wholesale brand:
When two or more confirmed orders exceed availability for a style, the units are allocated in this order: (1) accounts with an existing approved credit limit ahead of new accounts; (2) among those, in proportion to each order's quantity, rounded to the size run; (3) any remainder is offered as a substitute colorway or a later delivery window, in that order; (4) nothing is canceled without the rep speaking to the buyer first. The sales director decides; the booth manager records the decision and the reason on the ticket.
Note what the policy contains: a priority rule, a splitting rule, an escalation ladder, a named decision-maker, and a requirement to record the reason. The last one matters more than it looks, because in eight weeks somebody will ask why an account got 60 units instead of 100, and the answer needs to be a sentence recorded in the database that anyone can read back.
Your system's job is to detect the conflict, preserve both orders intact, and stop either one from silently overwriting the other. Picking a winner is a job for a person with commercial authority. Write the allocation policy before the show, name the person who applies it, and record the reason on every resolution. A system that resolves conflicts automatically by arrival time is making commercial decisions with a coin flip.
Failure playbook
Print this table and put it in the booth. Under pressure, nobody recalls a procedure they read once; they read the row and do what it says.
| What went wrong | Immediate action (at the booth, under 2 minutes) | Recovery (that evening) | Prevention |
|---|---|---|---|
| Device will not sync | Keep selling. Orders are safe locally. Check the chip says "Waiting to send", not "Needs attention". Toggle airplane mode once, then stop touching it. Do not reinstall, reset or wipe. | On hotel Wi-Fi, watch the queue drain to zero. If it will not drain, open the app's diagnostic screen and read the last error; a rejected write blocks everything behind it. Fix that one record, and the queue resumes. | Uploads process before downloads by default, so one poison record stalls the whole queue. Make the server accept-and-park anything it cannot process rather than rejecting it. |
| Device lost or stolen | Tell the booth manager and the show's security desk immediately. Manager marks the device lost in MDM and issues a remote lock. Rep takes the spare and keeps selling. | Write down what that device was holding, then revoke that rep's server-side sessions and rotate their credentials before you remote-wipe. Reconstruct unsynced orders from the appointment list and the reps' memory; call every buyer seen since the last sync. | Passcode plus biometrics, supervised enrollment, single-app mode, Activation Lock via Apple Business Manager (which ties the device to your organization so a thief cannot erase and resell it), and a printed device register with serials. Sync frequently enough that "since the last sync" is measured in minutes. |
| App crashes mid-order | Reopen. The draft should be there. If it is not, apologize once, and rewrite it from the buyer's own notes — buyers almost always have their picks written down too. | Pull the crash report. Do not ship a fix during the show; work around it and note it. | Persist every field change to SQLite as it happens, not on submit. A draft that only exists in memory is a draft you will lose. |
| Duplicate order created | Do not delete anything at the booth. Add a note to the newer order: "possible duplicate of the previous one". Carry on. | Query 4 in the reconciliation set finds them. Manager voids one with a reason code; the void is a new record, never a deletion. | Idempotency keys generated on the device (chapter 3), a unique id sent with the write so the server recognizes a repeat and ignores it, plus a disabled submit button after the first tap, and a duplicate warning when a second order for the same account is opened within 20 minutes. |
| Rep writes against stale prices | If caught at the booth, tell the buyer immediately and correct it. A price corrected in person is a small thing; a price corrected by invoice is a dispute. | Query for orders whose line prices differ from the current price list. Sales director decides per account whether to honor or correct. Every correction is a written note to the buyer. | Catalog freeze at T−14, price verification queries, and a price-list version stamped on every order line so a mismatch is a query rather than an argument. |
| A style sells out mid-show | The device shows a snapshot with a timestamp, so reps already know it is advisory. Manager announces "the olive parka is closed" to the booth. Reps offer the named substitute. | Resolve every pending line for that style through the allocation policy. Update the morning snapshot so tomorrow starts truthful. | Refresh the ATS snapshot at every break, not only overnight. Pre-agree a substitute for each hero style (the handful of styles you expect to carry the season) before the show, so reps never improvise. |
| Buyer disputes what was written | Open the order and show them the signed read-back and the timestamp. Almost every dispute ends there. | If there is no signature, the buyer is right. Amend the order, record why, and treat the missing signature as a process failure to fix tonight. | Signature plus read-back checkbox required before submit; emailed copy sent the same evening while memory is fresh. |
| iPad battery dies with unsynced orders | Plug into a bank. Wait. The data is in SQLite on flash storage and is not lost by a flat battery. Use the spare device meanwhile. | Once charged, open the app, let the queue drain, and reconcile against the rep's appointment list for the period since the last sync. | The 40% rule, four power banks, and a battery-percentage readout on the booth manager's dashboard so somebody is watching who is not also selling. |
| Venue Wi-Fi is captive and everything hangs | Turn Wi-Fi off entirely on all devices. Do not fight the portal. Fall back to the hotspot, or to pure offline. | If you must use it, log in once per device through the portal and turn off private Wi-Fi address for that SSID so the portal stops seeing a new device. Turn it back on after the show. | Never join venue Wi-Fi during show hours. Configure devices to prefer your hotspot's SSID and to forget every venue network. |
| Hotspot has no signal in the hall | Move it higher and toward the aisle. If that fails, tether one iPad from a rep's phone on the other carrier, sync that device, and let the rest stay offline. | Sync everything from the hotel. Note where in the booth signal was usable for tomorrow. | Second carrier, placement above head height, and an accepted plan that says a full show day offline is normal, not an incident. |
| The rep is signed out and cannot sign back in | Hand them the spare device, which is signed in. Do not try to fix the login in the hall; every sign-in method needs the network. | Sign the device back in on hotel Wi-Fi. Check whether a session time-box or inactivity timeout caused it, and turn it off for the rest of the show. | Sign in at T−7 and never sign out; remove or gate the sign-out button in the booth build; disable session time-boxing for the show window. |
One row deserves expanding because it is the least intuitive. "Device will not sync" is usually a poison record — one write the server refuses every time it is offered, and not a network problem at all.
PowerSync's troubleshooting guidance states that "by default, uploads are processed before downloads, so a backlogged upload queue delays receiving new data" — one write the server keeps rejecting sits at the head of the queue and blocks everything behind it indefinitely. PowerSync notes that buckets and streams at priority 0 are exempt from that blocking, at the cost of possible sync inconsistencies, so that is a lever and not a free fix.
The real design fix is server-side: accept anything structurally valid, park it for review if it fails a business rule, and return success so the queue advances. A server that errors on a business-rule violation converts a business problem into a stuck device.
Data hygiene
Records created in the field
Everything a rep creates at a show is created fast, by a tired person, in noise. Three rules cover most of it.
Minimum viable required fields. A prospect needs a name and one contact method; everything else is optional at capture and chased afterwards. A form with eleven required fields produces eleven filled fields, of which four say "N/A" and one says "same".
Capture raw, normalize later. Store what the rep typed exactly as typed and derive the cleaned version in a separate column. If a rep types "The Bluebird Boutique LLC (Bluebird)", keep that string forever as evidence, and put "bluebird boutique" in the normalized column your matching uses.
Always record who, when and where. Every field-created record carries the rep, the device, the show and the timestamp. When you untangle something in November, provenance is what makes it possible.
Duplicate detection on return
Two reps will meet the same buyer. Both create a prospect. Sometimes the buyer is already a customer under a slightly different name. Postgres has good tools for this; the one you want is pg_trgm, which measures similarity by counting shared three-character sequences (trigrams), so "bluebird boutique" and "blubird boutique" score high despite the typo.
-- Fuzzy duplicate detection for field-created prospects.
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Normalised key: lowercase, strip punctuation and legal suffixes.
CREATE OR REPLACE FUNCTION norm_name(t text)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
SELECT btrim(regexp_replace(
regexp_replace(lower(coalesce(t,'')),
'\y(llc|inc|ltd|limited|corp|co|gmbh|srl|bv|the)\y',
' ', 'g'),
'[^a-z0-9 ]', ' ', 'g'));
$$;
-- WARNING: adding a STORED generated column REWRITES the whole
-- table and holds an ACCESS EXCLUSIVE lock for the duration,
-- blocking every read and write against it. On a large customers
-- table that is an outage. Run this in a maintenance window,
-- never during a show, and time it on a copy first.
ALTER TABLE prospects
ADD COLUMN IF NOT EXISTS name_norm text
GENERATED ALWAYS AS (norm_name(legal_name)) STORED;
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS name_norm text
GENERATED ALWAYS AS (norm_name(legal_name)) STORED;
CREATE INDEX IF NOT EXISTS prospects_name_trgm
ON prospects USING gin (name_norm gin_trgm_ops);
CREATE INDEX IF NOT EXISTS customers_name_trgm
ON customers USING gin (name_norm gin_trgm_ops);
-- The % operator compares against pg_trgm.similarity_threshold,
-- which defaults to 0.3. Lower it to catch more (and review
-- more); raise it to catch fewer. This setting is per session.
SET pg_trgm.similarity_threshold = 0.35;
-- Candidate matches. Deliberately generous: a human reviews.
SELECT p.id AS prospect_id,
p.legal_name AS prospect_name,
c.id AS customer_id,
c.legal_name AS customer_name,
round(similarity(p.name_norm, c.name_norm)::numeric,2) AS name_score,
(lower(p.email) = lower(c.email)) AS email_match,
(p.tax_id = c.tax_id) AS tax_match,
(p.postcode = c.postcode) AS postcode_match
FROM prospects p
JOIN customers c
ON p.name_norm % c.name_norm -- trigram similarity
OR lower(p.email) = lower(c.email)
OR (p.tax_id IS NOT NULL AND p.tax_id = c.tax_id)
WHERE p.show_id = 'coterie-ss27'
AND p.merged_into IS NULL
ORDER BY tax_match DESC, email_match DESC, name_score DESC;
-- Prospect-to-prospect duplicates from the same show.
SELECT a.id, a.legal_name, a.created_by AS rep_a,
b.id, b.legal_name, b.created_by AS rep_b,
round(similarity(a.name_norm, b.name_norm)::numeric, 2) AS score
FROM prospects a
JOIN prospects b
ON b.id > a.id
AND a.show_id = b.show_id
AND (a.name_norm % b.name_norm OR lower(a.email) = lower(b.email))
WHERE a.show_id = 'coterie-ss27'
ORDER BY score DESC;
The norm_name function strips the noise that makes exact matching useless: case, punctuation, and the legal suffixes reps include or omit at random. Generated columns keep the normalized form in sync with the raw name without any application code — at the cost of a full table rewrite when you add them, which is why that warning is in capitals.
A GIN index is a Postgres index type built for values made of many small parts — here, the trigrams inside a name, and it is what makes the % similarity operator fast. Be aware that joining on three ORed conditions usually stops the query planner (the part of Postgres that chooses how to run a query) from using those indexes at all; on a customer table past a few hundred thousand rows, run the three signals as three separate queries and UNION the results instead.
The match query sorts so the strongest evidence surfaces first: a tax registration match is close to conclusive, an email match is strong, a name score alone is a suggestion. Expected output is a short list a human reviews in fifteen minutes.
Never merge automatically: two boutiques genuinely called "Bluebird" in different cities is a real thing, and merging them corrupts both accounts.
When you do merge, follow chapter 1's append-only discipline — you add new facts and never erase old ones. Write a merged_into pointer on the losing record, keep the record, and never delete it. Orders written against the losing record keep pointing at it and every report resolves the pointer, so an incorrect merge is undone by clearing one column rather than restoring a backup.
Getting the order book clean before it drives production
Show orders become factory commitments, and a factory commitment is money you cannot get back. Before the order book drives anything it passes a gate, which checks:
- Every order has a signature and a confirmed read-back, or a documented exception approved by the sales director.
- Every order is attached to a customer, not a prospect — meaning every prospect has been resolved to a new or existing customer record.
- Every line prices to the current verified price list, or has a recorded approved deviation.
- Every line has a ship start and cancel date within a valid window for that style.
- No request lines remain unresolved; each is approved and converted, or declined.
- Every oversell ticket is resolved with a recorded reason.
- No duplicate suspects remain unreviewed.
- Every order has a credit state, and orders on unapproved accounts are flagged for the production decision rather than silently included.
Implement the gate as a query returning a list of blockers, and treat "the query is empty" as the definition of a clean order book. A booking number from an ungated order book is a number that will change, and everyone downstream — production, finance, the factory — will plan against it anyway.
After the show
Order confirmations, and why email is a technical problem
Every buyer gets a written confirmation within 48 hours: a PDF of the order, the ship window, the terms, and a named person to reply to. Partly courtesy, mostly risk management — it is the last cheap moment to correct a misunderstanding.
Sending it reliably is real engineering work, because the large mailbox providers now enforce authentication. Before the requirements, three definitions. DNS (domain name system) is the internet's directory: you publish small records under your domain name and anyone can look them up.
A TXT record holds arbitrary text, which is how email policies are published. A CNAME record is an alias that says "for this name, go and look up that other name instead", which is how providers hand you a signing key they can rotate without asking you. An MX record says which server receives mail for a domain.
The current requirements, verified July 2026:
- Google requires SPF or DKIM for all senders, valid forward and reverse DNS, TLS for transmission, messages that conform to the internet message format standard (RFC 5322), and a spam rate below 0.3% in Postmaster Tools, Google's free reporting dashboard for senders. Senders of 5,000 or more messages a day to Gmail need SPF and DKIM and DMARC, a From: domain aligned with the SPF or DKIM domain, and one-click unsubscribe on marketing and subscribed mail. Effective 1 February 2024.
- Yahoo mirrors this: SPF or DKIM at a minimum; bulk senders need both plus a DMARC policy of at least
p=nonethat passes validation, unsubscribe requests honored within two days, and a spam rate below 0.3%. Enforcement began February 2024. Yahoo publishes no numeric volume threshold for "bulk", so do not assume Google's 5,000 applies. - Microsoft applies the same shape to Outlook.com, Hotmail.com and Live.com for domains sending more than 5,000 messages a day — SPF, DKIM and DMARC, with
p=noneas the minimum policy and alignment to SPF or DKIM. Enforcement began 5 May 2025 by routing non-compliant bulk mail to the junk folder; Microsoft has said non-compliant messages will later be rejected outright with550 5.7.515, on a date it has not yet announced.
What SPF, DKIM and DMARC each do
Three acronyms before you read that list again. SPF (sender policy framework) is a DNS record listing which servers are allowed to send mail as your domain. DKIM (domainkeys identified mail) is a cryptographic signature added to each message. DMARC (domain-based message authentication, reporting and conformance) ties those two results to the address the reader sees and tells receivers what to do when they fail.
A brand emailing a few hundred order confirmations sits below every 5,000-per-day threshold, so the strict bulk rules do not bite. Set all three records anyway: your marketing mail from the same domain will cross the threshold eventually, and unauthenticated transactional mail is far more likely to land in spam even below it.
An order confirmation in a buyer's spam folder is a dispute waiting eight weeks to happen.
DNS records for transactional mail. Values come from YOUR
email provider's dashboard - these are shapes, not literals.
Send from a subdomain so marketing volume cannot damage the
reputation of your order confirmations.
# SPF - which servers may send as send.yourbrand.com.
# Start with ~all (soft fail) while you are still finding
# forgotten senders, then tighten to -all (hard fail).
# SPF allows at most 10 DNS lookups: every include: costs one,
# and going over means SPF fails as "permerror" for everybody.
send.yourbrand.com. TXT "v=spf1 include:_spf.provider.com -all"
# DKIM - the public half of the key your provider signs with.
# Most providers give you a CNAME (an alias) so they can rotate
# the underlying key without you touching DNS again.
prov._domainkey.send.yourbrand.com. CNAME prov.dkim.provider.com.
# DMARC - what receivers should do when SPF and DKIM disagree
# with the visible From: domain, and where to send reports.
# A DMARC record on the parent domain also covers subdomains
# unless you add sp= to say otherwise.
# ONE TXT value; wrapped below only to fit this page.
_dmarc.yourbrand.com. TXT "v=DMARC1; p=none;
rua=mailto:dmarc@yourbrand.com; adkim=r; aspf=r"
# MX on the sending subdomain, ONLY if your provider asks for a
# custom bounce (MAIL FROM) domain. Use the host they give you.
send.yourbrand.com. MX 10 feedback-smtp.provider.com.
Rollout order (do not skip steps):
1. Publish SPF + DKIM. Send test mail. Confirm both PASS
in the raw headers of a message you received.
2. Publish DMARC at p=none. Collect rua reports for 2 weeks.
3. Read the reports. Find every system sending as your
domain that you forgot about (invoicing, CRM, calendar).
4. Move to p=quarantine. Wait 2 weeks. Read reports again.
5. Move to p=reject only when reports are clean.
Check with: dig +short TXT send.yourbrand.com
dig +short TXT _dmarc.yourbrand.com
dig +short CNAME prov._domainkey.send.yourbrand.com
What these do: SPF lists the servers permitted to send as your domain; DKIM cryptographically signs each message so a receiver can verify it was not altered in transit and did come from you; DMARC ties those two results to the visible From: address, tells receivers what to do when they fail, and asks for aggregate reports.
The -all at the end of SPF means "reject anything not listed" — the strict form, and the one you want once you are confident your sender list is complete; until then ~all fails softly and lets you read the reports first. The subdomain matters: sending confirmations from send.yourbrand.com keeps their reputation separate from bulk marketing, and the _dmarc record on the parent domain covers it automatically.
The staged rollout exists because going straight to p=reject is the classic way to discover, by silence, that your accounting system was sending invoices as your domain all along. The three dig commands verify the records are actually published — run them from a machine outside your own network, because internal DNS often answers differently.
Written orders to confirmed orders
The show produces written orders; production needs confirmed orders. Give the conversion a defined process and a deadline, because left alone it drifts for weeks.
| Stage | Meaning | Typical timing after show close | Who owns it |
|---|---|---|---|
| Written | Buyer committed at the booth; signed; in the system | Day 0 | Rep |
| Confirmed to buyer | Order PDF sent and acknowledged; corrections applied | Days 1–2 | Customer service |
| Credit cleared | Credit decision made; terms or prepay set on the account | Days 3–10 | Finance |
| Allocated | Units committed against the buy; oversells resolved | Days 5–12 | Merchandising |
| Production commitment | Order book locked; factory purchase orders raised | The cutoff date | Production |
The production commitment deadline is the important one: a date agreed before the show, printed on the order confirmation, and enforced. Its logic comes from the calendar in chapters F and G — fabric has a lead time, the factory has a booking slot, and both are lost if you hesitate. Past the cutoff, added units go into a later delivery or a reorder, never into the original buy (the quantity you commit to produce for the season).
Two practical notes. Cancellations before the cutoff are cheap and should be actively invited — a buyer who quietly reduces after you have cut fabric is far worse than one who tells you in week two, so say plainly in the confirmation email that changes are welcome until the cutoff.
And track the written-to-confirmed conversion rate per rep and per account; a rep whose orders systematically shrink between booth and cutoff is telling you something about how they sell.
The post-show hindsight
Hold it within two weeks, while people still remember, and separate the commercial review from the operational one.
The commercial review belongs to chapters E and L: sell-in (the units you sold to retailers, as opposed to the units they later sold to shoppers) by style and category, how much of the planned assortment actually got ordered, the styles that surprised you, average order value by account type, new accounts opened, and how many appointments turned into orders.
The operational review is yours, with a fixed agenda:
- Time to write an order. Measure it from the data, first line to submit, and take the median. There is no published industry benchmark to compare against, so the number that matters is your own: record it at the first show and watch whether it falls next season. Time spent in the software is time not spent selling, and it caps how many appointments a rep can take in a day.
- Every workaround that happened. Ask each rep directly: what did you have to do outside the software? Each answer is a feature request with evidence attached.
- Every offline minute. How long was each device disconnected, and did it matter? If it never mattered, the architecture is right and you can stop worrying about the network.
- Every review-queue ticket. How many, what caused them, how long to resolve. Rising oversells mean your snapshot refresh is too infrequent.
- Every paper form used. Zero is the target. Each one used is a failure with a story.
- Battery and power. Did anything run out? Was the bank count right? Replace the assumed runtime in the energy budget with what you actually observed.
- The kit. What did you carry and never use, and what did you wish you had? Update the packing checklist in the same meeting.
Write the answers in the same document every season. Three shows in, it is worth more than any vendor's best-practices guide, because it is about your booth, your reps and your buyers.
Training the booth team
Reps do not need to understand sync. They need to trust the system and know what to do when something looks wrong. Run the session in person at T−10, on the actual devices, structured as a drill rather than a demo:
- Five minutes: the mental model. "The iPad is your notebook. It always works, with or without signal. Everything you write is saved the instant you write it. The office turns your notes into confirmed orders overnight. You never wait for the network to sell."
- Five minutes: write a full order, with the device in airplane mode, in front of everyone. Deliberately include a new prospect and an out-of-catalog request, because those are the two paths reps improvise around.
- Five minutes: the chips. Show every status chip and say out loud what each one means and what the rep does about it. The answer for most of them is "nothing".
- Five minutes: the three escalations. Device dead, order looks wrong, buyer disputes something. Who they tell, in what order, and what they say to the buyer while it is being sorted.
Then have every rep do it themselves on their own device, offline, while you watch. Watching is the part people skip, and it teaches you what your interface actually communicates.
+----------------------------------------------------------+
| BOOTH CHEAT SHEET - SS27 / COTERIE / BOOTH 2214 |
+----------------------------------------------------------+
| |
| THE RULE: the iPad always works. Never wait for signal. |
| Everything you write is saved the moment you write it. |
| |
| BEFORE THE BUYER LEAVES - every single time: |
| [ ] Read back: units, value, ship window, terms |
| [ ] Read back every special note |
| [ ] Signature on glass |
| [ ] Email or print their copy |
| |
| STATUS CHIPS - what to do: |
| Writing / Saved on this iPad ....... nothing |
| Waiting to send ................... nothing |
| Sent - confirming ................. nothing |
| Office is checking ................ nothing |
| Needs attention ................... tell the manager |
| |
| STOCK NUMBERS are a SNAPSHOT with a time on them. |
| They are a guide, not a promise. Never promise stock. |
| Say: "I'll confirm availability today." |
| |
| BUYER WANTS SOMETHING NOT IN THE CATALOG: |
| Tap "Request" -> describe it -> photo -> quantity. |
| NEVER invent a style number. Ever. |
| |
| NEW ACCOUNT: tap "New prospect". Name + email is |
| enough. Write the order. Credit is not your problem |
| today; it never blocks writing an order. |
| |
| BATTERY: below 40% -> on a power bank, now. |
| |
| NEVER SIGN OUT of the app. If you are signed out, |
| take the spare device and tell the manager. |
| |
| IF THE IPAD DIES OR BREAKS: |
| 1. Take the spare from the back shelf. Keep selling. |
| 2. Tell the manager. Do not troubleshoot yourself. |
| 3. Never reset or wipe a device that has not synced. |
| 4. Total failure -> paper form -> PHOTOGRAPH IT |
| before the buyer walks away. |
| |
| NEVER join the venue wi-fi. Our network is BOOTH-2214. |
| |
| OVERSELL POLICY: existing credit accounts first, then |
| pro-rata by quantity, then substitute or later ship. |
| Sales director decides. Never promise an outcome. |
| |
| ESCALATE TO: |
| Booth manager .......... Dana, +1 917 555 0142 |
| Sales director ......... Ilse, +1 917 555 0198 |
| Systems ................ Ben, +1 917 555 0107 |
| Venue tech desk ........ 212 216 5432 |
+----------------------------------------------------------+
Laminate it, print one per rep plus two spares, and tape one to the back of the booth counter. Everything on it is either an instruction with a verb or a phone number.
Note how many status chips resolve to "nothing" — deliberate, because the most common booth failure is a rep intervening in a process that was working fine. The two "never" lines about signing out and wiping are there because both actions are irreversible in a hall and both look like reasonable troubleshooting to someone under pressure.
Adapt the numbers, booth code, SSID and oversell paragraph; keep the structure, because a rep reads this while walking and needs the right line in two seconds.
One staffing decision underpins all of this: appoint a booth manager whose revenue number for the show is zero. They watch battery levels, run the reconciliation queries, work the review queue, hold the spare device and take every escalation. On a three-rep booth that feels like a luxury and it pays for itself on the first afternoon, because the alternative is your best rep debugging a hotspot with a buyer waiting.
Field notes & further reading
- RFC 8910 — Captive-Portal Identification in DHCP and Router Advertisements — the standard that replaces traffic interception with an advertised API address, and the clearest official statement of why the legacy approach uses "methods that are very similar to man-in-the-middle (MITM) attacks". Pair it with RFC 8908 — Captive Portal API for the JSON contract a well-behaved portal serves.
- A First Look at Cellular Network Performance during Crowded Events (SIGMETRICS '13) — measured RRC failure rates and uplink throughput at two real large events on a US tier-1 network; the source for "2-3 orders of magnitude more RRC failures" near the venue and the 8x uplink spike. Measured on 3G, so read it for the mechanism.
- Cisco Meraki — Approximating Maximum Clients per Access Point — vendor client-count maximums by Wi-Fi generation, next to the vendor's own warning that real-world interference makes "this limit far lower".
- Jacob K. Javits Convention Center 2026 Exhibitor Price List (PDF) — current exhibitor pricing for power, Wi-Fi, wired drops and router rental. Read the internet pages once and the offline-first argument makes itself. Every venue publishes an equivalent.
- 49 CFR 175.10 — exceptions for passengers, crewmembers and air operators (Cornell LII) — the actual regulation behind the 100 Wh carry-on limit, the 101–160 Wh approval rule, the two-battery cap and the ban on spare lithium batteries in checked baggage. The official current text is on eCFR if you want the government copy.
- Supabase — User sessions — the one-hour access token default, single-use refresh tokens that do not expire on a timer, and the time-box and inactivity-timeout settings that will sign your reps out mid-show if you leave them on.
- PowerSync — Troubleshooting — the statement that uploads are processed before downloads, which is why one rejected write stalls a whole device, plus the priority-0 exemption. See also the protocol architecture page on checkpoints and resumable streams.
- Square — Process card payments with offline mode — the $1–$50,000 per-transaction cap, the 72-hour expiry, the unsupported payment types, and the sentence making you liable for expired, declined or disputed offline payments. Compare with Stripe Terminal's offline mode, and check senders against Google's email sender guidelines.
- Apple — iPad (A16) technical specifications — the source for the 28.93 Wh battery, the "up to 10 hours" Wi-Fi and "up to 9 hours" cellular ratings, the Wi-Fi 6 radio with no 6E, and the eSIM-only sentence. Pull the equivalent page for whatever device you actually buy and redo the energy budget with its numbers.
- PostgreSQL — pg_trgm — the trigram extension behind the duplicate-detection queries, including the 0.3 default for
pg_trgm.similarity_thresholdand how the%operator uses it.
1. Run a full offline rehearsal and measure it. Take a real device with a real synced dataset. Put it in airplane mode and leave it there for a full working day. Write twelve orders, including one new prospect, one out-of-catalog request line, and one order you deliberately submit twice. Pick a style with exactly 20 units left, write 15 of it on the first device, and write 10 of the same style on a second device — 25 units promised against 20 available, which is the oversell you want to see caught. Force-quit the app twice during the day, once after your access token has expired. At the end, reconnect both devices and run the six evening reconciliation queries. You are looking for four specific outcomes: no order lost, the duplicate detected by query 4 rather than by a person's memory, exactly one oversell ticket raised with both orders intact, and no login screen shown at any point while offline. Write down how long the median order took to write; that number is your real interface benchmark.
2. Build and price your own kit. Pull the exhibitor service kit PDF for a show you might actually do, and find the five things that matter: power, Wi-Fi per device, a wired drop, any router rental, and the advance-order deadline your organizer set. Put them in a spreadsheet next to the cost of your own hotspot plus a prepaid data plan priced on the day you read this. Then do the energy budget for your own team size using the worked example in this chapter, substituting your devices' real watt-hour ratings from their specification pages, and derive the number of power banks. Produce a one-page packing checklist and a one-page cheat sheet with your own phone numbers, SSID and oversell policy on them, and print both.
What "finished" looks like. You can hand a rep a charged, supervised, already-signed-in iPad and a laminated card, put them in a hall with no usable network for three days, and get back a complete, signed, reconcilable order book — with every conflict surfaced as a decision somebody made on purpose and recorded, and not one order lost, duplicated or silently overwritten. And you can say, from your own measurements rather than a vendor's brochure, exactly how many hours each device runs and how many power banks that requires.