UX research — client flow (landing → footage delivered)
Scope. The client journey only: / → /order → Stripe → /order/success →
/jobs/[id]/upload → /jobs / /jobs/[id] → /jobs/[id]/review. Depth on the
upload screen (B6), the make-or-break step. Every recommendation maps to a real
screen/component in docs/design/PAGE-SPECS.md (referenced as B1–B8, C1–C2) and
respects our architecture: the three ingest tiers (INGEST-SPEC §1), the
footage-readiness gate (job can't leave NEW until an original lands and no
ingest is mid-flight), authorize-now/capture-on-approval, and the unlisted
posture (no SEO furniture, invited audience).
Tags. Each recommendation is tagged:
[adopt now] cheap copy/layout, no new backend ·
[design change] needs Claude Design to redraw a component ·
[Phase 6+] needs backend not yet built (email, capture wiring, transcoder) ·
[reject] considered and declined, with reason.
Grounding note. Our uploader (uploader.tsx) uses Uppy + AwsS3 multipart
to R2, not the tus protocol. This matters for several findings below: multipart
resume works within a live session but does not survive a tab close/reload
today — which the current copy honestly admits ("only in this session").
Theme 1 — Large-file upload UX (the B6 screen)
What the best products do
- Frame.io makes resumability the headline promise: interrupted transfers "resume your upload with no loss of progress," and a centralized Upload Sheet exposes pause/resume/cancel and per-file + batch progress with transfer verification and logs. For single files over 2 GB it actively steers users to its dedicated Transfer app rather than the browser. (Frame.io Transfer docs, Uploading media)
- WeTransfer is the anti-pattern to learn from: closing the tab or dropping connection cancels the upload and forces a restart from zero; the free tier has no resumable uploads. Users are not clearly told when it's safe to leave. (Perkoon: WeTransfer upload failed)
- tus (the resumable-upload protocol behind Cloudflare Stream, bunny.net)
resumes via a
HEAD→Upload-Offsethandshake, retries network/5xx errors with exponential backoff, and — crucially — can persist upload progress in local storage so the user resumes even after closing the tab or restarting the browser, "but it requires a suitable UX to be understandable." Retries done silently mean "the user might not even notice an interrupted connection." (tus protocol, Uppy tus docs) - NN/G on waits: show a percent-done indicator for anything over ~10 s (lower the threshold when the estimate is uncertain); waits with feedback feel 11–15% faster and users tolerate longer waits and report higher satisfaction when a dynamic indicator is present. (NN/G progress indicators, Response-time limits)
What we do today (B6, uploader.tsx)
- Uppy Dashboard, AwsS3 multipart,
autoProceed:false. Note text: "large files resume automatically after interruptions." Sub-copy: "Keep this tab open while uploading — transfers resume automatically after interruptions, but only in this session." Honest, but the constraint is buried in one grey line. - Progress is whatever the stock Uppy Dashboard renders (per-file bars). No aggregate progress across a multi-file drop, no time-remaining estimate, no explicit "you can pause" affordance surfaced in our own chrome.
- No cross-session resume: a marina-Wi-Fi client who backgrounds Safari on their phone loses the multipart session.
Recommendations
Make the "keep this tab open" constraint load-bearing, not a footnote.
[design change]Promote it to a small persistent banner inside the Uppy re-skin (brief §7 allows CSS re-skin only), with an icon and one line: "Keep this tab open until uploads finish — they'll resume after a hiccup, but only while this tab is open." This is the single most important honesty on the page for our persona (mobile, patchy marina Wi-Fi, DESIGN-BRIEF §2). Pair it with the opposite promise on the ingest panel (already present: "Safe to close this tab") so the two paths are visibly, deliberately different.Add an aggregate progress + time-remaining line above the Dashboard.
[design change]The stock per-file bars satisfy NN/G's percent-done rule per file, but a client dropping 40 GB across 12 clips has no single "how far am I" signal. Design one summary line ("4 of 12 files · 31 GB of 44 GB · ~18 min left") driven by Uppy'supload-progress/progressevents. Uncertain ETAs should round generously and never count up — NN/G says variance argues for showing the reassurance sooner, not a precise-looking number that then slips.Evaluate cross-session resume for browser uploads.
[Phase 6+]The real gap vs Frame.io is that our multipart session dies on tab close. Two options, in cost order: (a) persist the R2uploadId+ completed parts inlocalStorageand rehydrate the Uppy AwsS3 upload on return (Uppy exposes the multipart callbacks we already implement —listPartsis the resume hinge); (b) longer-term, weigh a tus-backed path. Until then, do not weaken the "only in this session" copy — it's currently true and honest. Flag for a Phase-6 spike; not required for the Phase-2b gate.Surface resumability as a promise, not just recovery.
[adopt now]Frame.io sells "resume with no loss of progress" as a feature up front. Our/home page (MARKETING-PAGES §1 step 2) and the B6 heading can say "large uploads survive a dropped connection" — this is trust built before the hours- long wait, which is when the client is deciding whether to believe us.Keep failure messages concrete and calm at the file level.
[design change]The Uppy re-skin must keep Uppy's own per-file error affordance legible in-theme (PAGE-SPECS B6 already requires this). Add a single retry affordance per errored file rather than an all-or-nothing restart — the tus research shows silent/one-click retries keep users from perceiving the network as broken.
Theme 2 — Alternative-source ingest & choice architecture (B6)
What the best products do
- MASV Portals present one primary drag-and-drop surface, no account needed, then route behind the scenes to 25+ storage integrations — the complexity is hidden from the sender, who only ever sees "drop files, hit send." (MASV Portals, Cloud connections)
- Hick's Law / choice overload: decision time and dissatisfaction both rise with the number of visible options; the standard remedies are prioritize one primary action visually and progressive disclosure — reveal secondary paths in stages rather than showing all at once. (Laws of UX: Hick's Law, Webflow: Hick's Law)
What we do today (B6)
- Three affordances. Two of them already live inside one Uppy Dashboard (drag-drop + "Connect a service" pickers), and the paste-link panel sits below. So we're closer to MASV's "one surface" than the brief's "three side-by-side" literal reading — good.
- The connect-a-service pickers appear only when Companion is configured
(
NEXT_PUBLIC_COMPANION_URL), so the option count is naturally 2 or 3, never fixed. - The paste-link panel is a co-equal
<h2>("Or paste a transfer link"), same visual weight as the Dashboard.
Recommendations
Rank the three paths; don't present them as equals.
[design change]Apply Hick's Law: drag-drop is the primary path for a client who has the files on the device in front of them; give it the most weight. "Connect a service" is inside the Dashboard already (good — it reads as a secondary tab, not a third top-level choice). Demote the paste-link panel to a quieter, collapsible "Footage somewhere else? Paste a link" disclosure rather than a full-weight<h2>section — it's the fallback path, and most clients won't need it. This keeps the visible choice count at ~1 primary + 2 progressive.Keep the manual fallback framed as service, surfaced late — never as an error.
[adopt now]Thequeued_manualchip copy is already right ("our team will handle this", PAGE-SPECS A2b, brief §4). Reinforce this at the point of paste too, not only after failure: the sub-copy "if we can't fetch it automatically, our team will" (already iningest-panel.tsx) is the exact MASV-style "complexity is our problem" reassurance — keep it verbatim. The manual path should never be a button the client chooses; it's an automatic degradation the server decides (INGEST-SPEC §1 Tier 3). Confirmed correct.Never show the raw pasted URL back.
[adopt now]Already enforced (source_display= redactedhost · filename;source_urlcolumn-revoked, PAGE-SPECS B6). This is both a privacy requirement (share links are credentials, INGEST-SPEC §4) and, incidentally, a cleaner UI. No change — flag only that Claude Design must not "helpfully" echo the link in any hover/tooltip.Distinguish the two "safe to close" postures explicitly and symmetrically.
[design change]This is load-bearing (brief §4) and easy to get subtly wrong. Drag-drop = "keep tab open"; paste-link = "safe to close." Give each a matching small inline status icon + one line so a non-technical client reads the difference at a glance, rather than inferring it. (See rec 1.)
Theme 3 — Status & anticipation UX for a multi-day service (B4, B5, /jobs)
What the best products do
- Domino's Pizza Tracker: five named stages (Order Placed → Prep → Bake → Quality Check → Out for Delivery) surfaced from back-end state the company was already tracking. Result: reduced anxiety, a sense of control, fewer support calls, and a credited sales lift. The lesson is granularity that reassures — enough stages to show motion, not so many that a stall becomes visible. (The Hustle: Domino's tracker, OvationCXM case study)
- Uber uses push notifications at threshold moments (driver matched, "a few minutes away," "arrived") plus a live map, and proactively detects stalls ("driver not moving") to reassure before the user panics — push at milestones, pull-map for the in-between. (Uber UX perspective)
- Buell & Norton's "labor illusion" (Harvard, Management Science 2011): when a service shows the work happening, people perceive higher value and reciprocity — they sometimes prefer a visible-effort wait to an instant result. Operational transparency is a value lever, not just an anxiety patch. (HBS: The Labor Illusion)
What we do today
/jobs(B4) and/jobs/[id](B5) render the raw enum status to everyone today — PAGE-SPECS already flags this as an elevate: apply the client label map (A1) soNEW→"Order received",IN_REVIEW/QC_REJECTED→"Quality check",CLIENT_REVIEW→"Ready for your review", etc.- Status is pull-only: the client must open
/jobsto learn anything. No push, no email on state change (email is Phase 7). - The
NEW-state footage-readiness banner is our one genuinely transparent, concrete cue ("Footage incoming — 1 import pending").
Recommendations
Ship the client label map (A1) as the anti-anxiety spine.
[adopt now]This is the Domino's move: a small, calm set of named states, deliberately coarse. The A1 mapping is already designed to hide internal churn —QC_REJECTEDandIN_REVIEWboth read "Quality check" so rework is invisible (brief §4). Adopt exactly as specced; it's the single highest-leverage calm change on the pull surfaces.Render status as a horizontal stepper, not just a chip, on
/jobs/[id].[design change]Domino's power is the sense of a journey with a known end. Map the client-label states to a 4-step visual: Order received → In production → Quality check → Ready for review → Delivered (collapse ASSIGNED/IN_PROGRESS into "In production", per A1). The chip says where; the stepper says how far and how much is left. Keep it hairline-quiet (brief §6) — the chip stays the loudest element. Do not expose per-editor micro- stages (labor illusion tempts over-sharing, but identity privacy and calm win here — brief §2).Add milestone push via email at 3 moments only.
[Phase 6+]Uber's lesson is push-at-milestones, pull-for-the-rest. Once Resend lands (Phase 7), notify the client on exactly: (a) footage received / production started, (b) "Ready for your review" (the one status that asks them to act — A1 marks it accent/action), (c) Approved & finals ready. Nothing in between — over-notifying a multi-day wait manufactures anxiety. Everything else stays pull (/jobs). This respects the unlisted/low-ceremony posture.Keep the footage-readiness banner concrete, and consider echoing it on the upload page.
[design change]"Footage incoming — {n} import(s) pending" is exactly the operational-transparency the labor-illusion research rewards: it shows the machine working. PAGE-SPECS B6 already suggests surfacing the samegetFootageReadiness()line on the upload page — do it, so the client sees "we'll start once everything lands" while their imports run, closing the loop between "I sent it" and "they have it."A stalled ingest should self-explain, Uber-style.
[adopt now]Afailedchip today shows an ⓘ tooltip with the error. For a non-technical client that's thin. When an ingest fails and degrades toqueued_manual, the row should read as reassurance the platform noticed: "We couldn't fetch this automatically — our team will import it for you." (This is a copy change to the failure→manual transition surface, not a new state.)
Theme 4 — Premium-service checkout: authorize-now, charge-on-approval (B2, B3, C2)
What the best products do
- Payment-industry guidance on authorization holds is blunt: the #1 failure is customer confusion when a hold appears without a clear explanation. Best practice is to tell customers upfront — on the payment page and in confirmations — when a hold is placed, for how much, and when the real charge happens; framing the hold's temporary nature is what builds trust. (Chargebacks911: authorization holds, Clearly Payments: auth & capture)
What we do today
/order/success(B3) leads with "Payment authorized ✓" and makes "your card was authorized, not charged — capture happens only on approval" the trust anchor. Strong, and correctly the visual focus (PAGE-SPECS B3 AC1).- But
/order(B2) — the moment before the client commits at Stripe — does not state the hold-not-charge promise. The reassurance currently arrives only after payment, which is one step too late for the anxious first-timer. - C2 review page (Phase 6) is where the capture actually happens on Approve; PAGE-SPECS already specs a deliberate, confident confirm.
Recommendations
Move the "authorized, not charged" promise upstream to the
/orderbutton context.[adopt now]Put a single quiet line directly under the primary button: "You won't be charged today — we authorize your card now and only charge when you approve the final edit." The industry research is explicit that the reassurance belongs on the payment page, before commitment, not only on the receipt. This is pure copy; no backend change. Reuse the exact B3 wording for consistency.Show the amount the hold is for, and that rush is included in it.
[adopt now]PAGE-SPECS B2 already wants the rush surcharge shown as a line item before commit. Frame the total as "authorization hold: £X (incl. rush)" so the number the client later sees as "pending" on their statement matches what we told them — the hold-amount mismatch is a top confusion driver in the research.Make Approve (C2) the confident capture moment, with hold→charge stated plainly.
[Phase 6+]When the client approves, the confirm should close the loop opened at/order: "Approving releases your payment (the hold you authorized becomes a charge) and unlocks your final files." PAGE-SPECS C2 already specs a deliberate accent-led confirm; this is the copy that connects it back to the authorize step. Note the current permission matrix makesCLIENT_REVIEW→APPROVEDadmin-only until the Phase-6 capture wiring — don't ship the client Approve before that.On
/order/successwhen the webhook is still landing, keep it calm.[adopt now]The "Setting up your job… refresh your jobs →" branch (B3) must never read as a payment failure — it's expected webhook latency. Confirmed correct in spec; flag only that the copy must not say "pending" or "problem."
Theme 5 — Mobile-first brief form (B2)
What the best products do
- Baymard: single-column forms outperform multi-column (fewer errors, higher completion); for long forms, split into steps and collapse/grey future sections — completing a section "creates a sense of progress on what would otherwise feel like an endless column." Show progress by section, not per question. (Baymard: form design, UX Design World: long mobile forms)
- General mobile-form guidance: correct input types/keyboards, thumb-reachable primary action, minimal typing. (Zuko: mobile form UX)
What we do today (B2, order-form.tsx)
- Single column, mobile-first, one field per row (PAGE-SPECS A5) — already aligned with Baymard's core finding.
- The brief is a tenant-configured JSON template of arbitrary length (text/textarea/select/multiselect/date). A rich Wakecut brief could be long on a 360px phone, presented as one unbroken scroll with package chooser + rush + VAT + submit all on one page.
- No save-progress / draft persistence. A client interrupted mid-brief (a call, a lost tab) loses everything typed. No autosave.
- Validation errors render as a crimson list keyed to
"{label}" is required.
Recommendations
Group the brief into labelled sections; keep it one page but chunked.
[design change]Don't force a wizard (the brief is short-to-medium and a multi-step flow adds ceremony the persona doesn't want), but apply Baymard's "collapse future sections" idea lightly: visually group Package → Your brief → Rush & billing with hairline dividers and section headers so the phone user perceives progress and structure instead of an endless column. This is a layout change toorder-form.tsx, no data change.Autosave the in-progress brief to
localStorage.[design change]The highest-value mobile-form fix for our persona: persistbriefData+ selected package tolocalStorageon change, rehydrate on return, clear on successful submit. A charter manager filling a brief on marina Wi-Fi will get interrupted; losing a half-written creative brief is a silent conversion killer. No backend — it's client-state only. (Distinct from Theme 1's upload resume, but the same "survive the interruption" principle.)Right keyboard per field type; required markers not colour-only.
[adopt now]type=date→ native date picker,type=urlon the transfer-link input (already), email inputsinputmode. PAGE-SPECS A5 already requires accessible required markers (not the crimson*alone) — confirm Claude Design ships an(required)text/aria affordance, per WCAG AA (brief §7).Validation: render
data.detailsas a fixable checklist, preserve typed input.[adopt now]Already specced (B2). Add: never clear the form on a validation or network error — same "don't lose the client's work" rule as C2's note field. Scroll to the first error on mobile.
Cross-cutting: the unlisted, premium, engine-neutral posture
- No dark patterns, no urgency theatre. The persona is premium and invited;
the aesthetic is "discreet, expensive machinery" (brief §5–6). Reject any
countdown timers, fake scarcity, or "X people uploading now" — they'd read as
cheap.
[reject: brand-inappropriate] - No public trust badges / SEO furniture. Unlisted product (brief §7);
the trust strip on
/is factual and short (MARKETING §1.4). Don't import the listicle "add security seals" advice — our trust is the hold-not-charge promise, EU/GDPR storage, and watermarked previews, stated plainly.[reject: unlisted posture] - Everything tenant-config, nothing yacht-hardcoded. All copy above lives in engine defaults / tenant slots (brief §5) — no recommendation introduces a hardcoded vertical string.
Top-10 prioritized recommendations
Ranked by impact ÷ cost, upload-screen-weighted (B6 is the make-or-break step).
| # | Recommendation | Screen | Tag | Why it ranks here |
|---|---|---|---|---|
| 1 | Ship the client status label map (A1) everywhere | B4, B5 | adopt now | Removes raw enums; the calm spine of the whole flow. Cheap, already specced. |
| 2 | Promote "keep this tab open" to a real in-Dashboard banner; mirror "safe to close" on paste-link | B6 | design change | The one honesty that saves a marina-Wi-Fi upload. Load-bearing (brief §4). |
| 3 | Move "authorized, not charged" promise upstream to the /order button |
B2 | adopt now | Research says the hold reassurance belongs before commit; pure copy. |
| 4 | Autosave the in-progress brief to localStorage |
B2 | design change | Silent conversion killer removed; no backend. |
| 5 | Aggregate progress + honest time-remaining line above the Dashboard | B6 | design change | NN/G: feedback makes hours-long waits feel 11–15% faster. |
| 6 | Rank the three ingest paths (drag-drop primary, paste-link collapsible) | B6 | design change | Hick's Law; MASV "one surface" model; cuts choice paralysis. |
| 7 | Status stepper (journey view) on /jobs/[id] |
B5 | design change | Domino's effect: shows distance-to-done, cuts anxiety + support pings. |
| 8 | Milestone email push at 3 moments only (footage in, ready-for-review, approved) | B4/B5 | Phase 6+ | Uber's push-at-milestones; needs Resend (Phase 7). |
| 9 | Cross-session resume for browser uploads (persist multipart state) | B6 | Phase 6+ | Closes the real gap vs Frame.io; don't weaken honest copy until shipped. |
| 10 | Group the brief into hairline-divided sections | B2 | design change | Baymard: perceived progress on a long mobile brief; layout-only. |
Confirmed-correct (no change needed): redacted ingest source display (B6);
queued_manual framed as service not error (A2b); /order/success
authorized-not-charged as the visual focus (B3); single-column mobile brief (A5);
manual fallback as automatic server degradation, never a client-chosen button.