A working concept, labeled honestly

Stopwise is a concept product built by Flickode to show what a field-crew app and its office board can do. It isn't a real company and has no customers.

What's real

  • The phone is the real Stopwise app (Expo / React Native), built for the web from the same code that builds for iPhone and Android.
  • Every change you make goes through a real API into a real Postgres database, and the office board reads the same data.
  • Voice notes and label photos go to real AI models. Each result says which model made it, and there is a daily limit per visitor.
  • The phone keeps working with no signal and syncs when it's back.

What's simulated

  • The company, the crew, the customers and the jobs are fictional. The town of Marlow Bay doesn't exist.
  • The other crew members are simulated. Only the technician on the phone is you.
  • Push notifications, texts, emails, webhooks and payments are simulated and labeled where they appear. Payments say “Test mode, no real charges”.
  • The demo day starts at 10:40 AM and runs 20 times faster than real time.

Privacy

  • One functional cookie keeps your private copy of the demo. No analytics or advertising cookies.
  • Nothing personal is collected. The demo uses sample photos and recordings, never your camera or microphone.
  • If you type a note for the AI, phone numbers and email addresses are removed first. Please don't type real names or addresses.
  • Your demo is deleted after a day without use, or right away with Reset demo.
Stopwise

A working concept, labeled honestly

Stopwise is a concept product built by Flickode to show what a field-crew app and its office board can do. It isn't a real company and has no customers.

What's real

  • The phone is the real Stopwise app (Expo / React Native), built for the web from the same code that builds for iPhone and Android.
  • Every change you make goes through a real API into a real Postgres database, and the office board reads the same data.
  • Voice notes and label photos go to real AI models. Each result says which model made it, and there is a daily limit per visitor.
  • The phone keeps working with no signal and syncs when it's back.

What's simulated

  • The company, the crew, the customers and the jobs are fictional. The town of Marlow Bay doesn't exist.
  • The other crew members are simulated. Only the technician on the phone is you.
  • Push notifications, texts, emails, webhooks and payments are simulated and labeled where they appear. Payments say “Test mode, no real charges”.
  • The demo day starts at 10:40 AM and runs 20 times faster than real time.

Privacy

  • One functional cookie keeps your private copy of the demo. No analytics or advertising cookies.
  • Nothing personal is collected. The demo uses sample photos and recordings, never your camera or microphone.
  • If you type a note for the AI, phone numbers and email addresses are removed first. Please don't type real names or addresses.
  • Your demo is deleted after a day without use, or right away with Reset demo.
Start the live demo

Under the hood

The architecture, the data model, every endpoint, your own demo's webhook and job log, and the tests of the build you are looking at. Nothing here is a mockup.

Architecture

One Expo codebase for the phone, one Next.js app for the pages and the API, one Postgres database. Hover, tap or tab through the boxes.

Technician appExpo · React NativeOffice boardNext.js pagesAPINext.js route handlers · Vercel FunctionsPostgresNeon · Drizzle ORMAI modelsVercel AI GatewayHourly cleanupVercel CronPush · SMS · email · webhooks · paymentsSimulated, logged, labeled

API

Next.js route handlers · Vercel Functions

Checks the signed token, takes a per-sandbox row lock, applies each change with the same reducer the phone uses, and records events, notifications and webhooks in one transaction. Repeated changes are answered from the idempotency log.

Where it lives

  • apps/web/src/server/sync.ts
  • apps/web/src/server/lock.ts
  • packages/core/src/apply-op.ts

Data model

Generated from the Drizzle schema of this build: 13 tables. Everything a demo owns hangs off sandboxes with a composite key and a cascading delete.

sandboxes

  • id · pkuuid
  • visitor_idtext
  • industrytext
  • for_keytext
  • for_nametext
  • accenttext
  • signedboolean
  • generationinteger
  • epochinteger
  • last_seqbigint
  • clock_started_attimestamp with time zone
  • sim_stepinteger
  • next_sim_due_mininteger
  • ops_daytext
  • ops_todayinteger
  • resets_daytext
  • resets_todayinteger
  • created_attimestamp with time zone
  • last_seen_attimestamp with time zone

crew_memberspk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • nametext
  • initialstext
  • roletext
  • is_visitorboolean
  • color_indexinteger
  • map_xinteger
  • map_yinteger

customerspk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • nametext
  • phonetext
  • emailtext
  • addresstext
  • map_xinteger
  • map_yinteger
  • notestext

eventspk (sandbox_id, seq)

  • sandbox_id → sandboxesuuid
  • seqbigint
  • job_idtext
  • actor_kindtext
  • actor_nametext
  • typetext
  • payloadjsonb
  • simulatedboolean
  • sim_stepinteger
  • demo_minuteinteger
  • created_attimestamp with time zone

job_runspk (id)

  • iduuid
  • sandbox_id → sandboxesuuid
  • nametext
  • run_keytext
  • statustext
  • started_attimestamp with time zone
  • finished_attimestamp with time zone
  • detailsjsonb
  • simulatedboolean

jobspk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • numbertext
  • customer_id → customerstext
  • crew_member_id → crew_memberstext
  • statustext
  • prioritytext
  • typetext
  • window_start_mininteger
  • window_end_mininteger
  • route_orderinteger
  • amount_centsinteger
  • versioninteger
  • updated_seqbigint
  • completed_at_mininteger
  • docjsonb

join_codespk (code_hash)

  • code_hashtext
  • sandbox_id → sandboxesuuid
  • generationinteger
  • expires_attimestamp with time zone
  • used_attimestamp with time zone

notificationspk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • seqbigint
  • audiencetext
  • channeltext
  • titletext
  • bodytext
  • job_idtext
  • payloadjsonb
  • statustext
  • created_attimestamp with time zone
  • read_attimestamp with time zone

paymentspk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • job_idtext
  • amount_centsinteger
  • statustext
  • modetext
  • provider_reftext
  • created_attimestamp with time zone
  • updated_attimestamp with time zone

sync_opspk (sandbox_id, op_id)

  • sandbox_id → sandboxesuuid
  • op_idtext
  • payload_hashtext
  • device_idtext
  • job_idtext
  • typetext
  • statustext
  • resultjsonb
  • created_attimestamp with time zone

webhook_deliveriespk (sandbox_id, id)

  • sandbox_id → sandboxesuuid
  • idtext
  • seqbigint
  • event_typetext
  • target_urltext
  • signaturetext
  • status_codeinteger
  • attemptinteger
  • simulatedboolean
  • payloadjsonb
  • created_attimestamp with time zone

usage_counterspk (scope, day)

  • scopetext
  • daytext
  • countinteger
  • micro_usdbigint

webhook_eventspk (event_id)

  • event_idtext
  • received_attimestamp with time zone

API

Tokens are signed per demo and per role (office or phone). The phone's writes are idempotent: every change carries its own id, and a replay gets the same answer.

API endpoints
EndpointCalled byWhat it doesRun
POST/api/sessionPageFinds or creates your private demo; returns an office token and a phone token.
POST/api/session/resetOfficeStarts the day over for this visitor's demo (new generation).
POST/api/sync/pushPhoneQueued changes with unique ids; applied in order, answered with an ack per change.
GET/api/sync/pull?cursor=PhoneEverything that changed since the phone's cursor, plus notifications.
GET/api/office/board?since=OfficeThe board snapshot, or 204 when nothing changed since the given sequence.
POST/api/office/jobsOfficeCreates an urgent job from a template and pushes it to the phone (simulated push).
GET/api/office/jobs/:idOfficeOne job with its evidence and timeline.
POST/api/office/jobs/:id/assignOfficeAssigns a job; the phone gets a simulated push.
POST/api/office/jobs/:id/send-reportOfficeBuilds the customer email. It would be sent; nothing leaves the demo.
GET/api/under-the-hood/logsOfficeThis demo's webhooks, job runs and notifications, plus cron runs.
POST/api/ai/transcribePhoneSample voice note → speech-to-text (capped; cached transcript as fallback).
POST/api/ai/voice-reportPhoneTranscript → structured report (capped; scripted or rule-based fallback).
POST/api/ai/label-scanPhoneSample label photo → vision model → fields with format checks.
POST/api/payments/intentPhoneTest-mode payment: simulated, or Stripe test mode when test keys are set.
POST/api/webhooks/stripeStripeSignature-checked; exists only when Stripe test keys are configured.
GET/api/cron/cleanupVercel CronHourly; deletes demos unused for a day. Needs the cron secret.
GET/api/healthAnyoneDatabase, AI and capacity status.

Running a request uses your private demo; if you don't have one yet, it is created (the same as opening the live demo).

Webhooks and jobs

What the back end did for your demo. Integrations that would call the outside world are simulated and tagged; the hourly cleanup job is real and shared by all demos.

Your demo's log shows what the back end did for you: webhook deliveries, background jobs and notifications. Use the live demo first, then load it here.

Tests

The unit and API tests ran while building this deployment (commit e72f52d, built Tue, 29 Sep 2026 01:55:42 UTC); the build fails if any test fails. The smoke and end-to-end results are the latest runs against the live site, labeled with the build they tested.

315/318

unit and API tests passed (3 database-concurrency tests run separately against Neon)

Post-deploy smoke test

9/9 checks against the live site (build 5355634) · Tue, 29 Sep 2026 01:23:31 UTC

  • health passeddb ok, ai ok, mode normal, build 5355634
  • session passedMarlow Bay Heating & Air
  • board snapshot passedasOfSeq 16
  • phone change reaches the board passedpush→board 899 ms
  • AI label scan answered by the model passedai openai/gpt-4.1-nano · 6 fields
  • device app shell passed/device/_expo/static/js/web/entry-54b6a3c92c768d8bdb9b64bc172ac8ac.js
  • device bundle passed
  • device deep link serves the shell passed
  • missing device chunk is a 404 passed

End-to-end (Playwright)

20/20 passed · against the live site (build 03736e5) · Tue, 29 Sep 2026 01:14:48 UTC

  • main flow (hvac): arrive, work with no signal, sync, complete passed30.7 s
  • main flow (pest): arrive, work with no signal, sync, complete passed26.0 s
  • main flow (inspection): arrive, work with no signal, sync, complete passed10.6 s
  • main flow (sales): arrive, work with no signal, sync, complete passed31.8 s
  • dispatch: an urgent job reaches the phone as a simulated push passed15.5 s
  • personalized proposal link: ribbon, company name, accent, noindex passed12.0 s
  • industry switch keeps offline changes and syncs them to the right demo passed17.5 s
  • reset starts the day over and the phone follows passed15.6 s
  • screenshot / at 390px, no console errors passed4.9 s
  • screenshot /demo at 390px, no console errors passed17.1 s
  • screenshot /office at 390px, no console errors passed6.1 s
  • screenshot /under-the-hood at 390px, no console errors passed4.0 s
  • screenshot / at 1366px, no console errors passed3.8 s
  • screenshot /demo at 1366px, no console errors passed3.3 s
  • screenshot /office at 1366px, no console errors passed9.4 s
  • screenshot /under-the-hood at 1366px, no console errors passed6.8 s
  • screenshot / at 1440px, no console errors passed6.4 s
  • screenshot /demo at 1440px, no console errors passed9.8 s
  • screenshot /office at 1440px, no console errors passed4.9 s
  • screenshot /under-the-hood at 1440px, no console errors passed5.2 s
Shared domain logicDomain logic shared by the phone and the server221/221 · 14 files
packages/core/test/accent.test.ts24/24
  • client accent derivation › keeps every derived pair readable for #006c4a4 ms
  • client accent derivation › keeps every derived pair readable for #E11D481 ms
  • client accent derivation › keeps every derived pair readable for #FACC155 ms
  • client accent derivation › keeps every derived pair readable for #0EA5E93 ms
  • client accent derivation › keeps every derived pair readable for #1111110 ms
  • client accent derivation › keeps every derived pair readable for #7C3AED0 ms
  • client accent derivation › keeps every derived pair readable for #F973161 ms
  • client accent derivation › keeps every derived pair readable for #22C55E1 ms
  • client accent derivation › keeps every derived pair readable for #FFFFFF2 ms
  • client accent derivation › keeps every derived pair readable for #9CA3AF1 ms
  • client accent derivation › keeps every derived pair readable for #1E40AF0 ms
  • client accent derivation › keeps the brand green as-is with white text0 ms
  • client accent derivation › flags #C0143C as too close to a status color0 ms
  • client accent derivation › flags #E11D48 as too close to a status color0 ms
  • client accent derivation › flags #DC2626 as too close to a status color0 ms
  • client accent derivation › flags #16A34A as too close to a status color1 ms
  • client accent derivation › flags #22C55E as too close to a status color1 ms
  • client accent derivation › lets #0EA5E9 drive primary actions1 ms
  • client accent derivation › lets #7C3AED drive primary actions0 ms
  • client accent derivation › lets #1E40AF drive primary actions0 ms
  • client accent derivation › lets #F97316 drive primary actions1 ms
  • client accent derivation › uses a darker fill when neither white nor ink reaches 4.5:11 ms
  • client accent derivation › gives a soft tint for selected states1 ms
  • client accent derivation › rejects an invalid hex1 ms
packages/core/test/ai-label.test.ts6/6
  • label scan helpers › returns the label's ground truth as the scripted result3 ms
  • label scan helpers › passes every format check on the ground truth1 ms
  • label scan helpers › flags a serial that doesn't match the manufacturer's format1 ms
  • label scan helpers › flags a field the model could not read0 ms
  • label scan helpers › keeps only known fields from model output, trimmed and capped0 ms
  • label scan helpers › rejects oversized values in the model output schema3 ms
packages/core/test/ai-text.test.ts11/11
  • sanitizeAiText › strips links, email addresses and phone numbers the model might invent3 ms
  • sanitizeAiText › replaces denied words and collapses whitespace0 ms
  • redactPii › masks phones and emails in typed notes before AI and storage0 ms
  • redactPii › leaves measurements and model numbers alone0 ms
  • sanitizeReport › cleans every text field of a model report1 ms
  • rule-based extractor (fallback when AI is unavailable) › finds parts, amounts, measurements and a follow-up in an HVAC note13 ms
  • rule-based extractor (fallback when AI is unavailable) › reads quantities written as words and amounts in other units1 ms
  • rule-based extractor (fallback when AI is unavailable) › turns inspection notes into findings with severity1 ms
  • rule-based extractor (fallback when AI is unavailable) › captures an order from a sales visit1 ms
  • rule-based extractor (fallback when AI is unavailable) › always returns a valid report, even for an unhelpful note7 ms
  • rule-based extractor (fallback when AI is unavailable) › never echoes contact details from the note1 ms
packages/core/test/apply-op.test.ts31/31
  • status changes › moves en route → on site with a feed event9 ms
  • status changes › treats a repeated tap as a no-op without new events1 ms
  • status changes › refuses to skip steps1 ms
  • status changes › blocks completion until required items, signature and payment are in, naming what's missing1 ms
  • status changes › completes with events and customer/webhook effects once everything is in1 ms
  • status changes › lets the worker undo arrival and completion1 ms
  • status changes › reports a conflict when the office cancelled the job1 ms
  • status changes › only lets the office cancel0 ms
  • checklist last-writer-wins › ignores an older change that arrives after a newer one1 ms
  • checklist last-writer-wins › breaks timestamp ties by device id, then op id1 ms
  • checklist last-writer-wins › caps a device clock that runs ahead at the server's receive time1 ms
  • checklist last-writer-wins › rejects an unknown checklist item1 ms
  • evidence ops › adds a catalog item once per op id, priced from the catalog1 ms
  • evidence ops › rejects items that aren't in the catalog0 ms
  • evidence ops › removes an item, and ignores removing one that isn't there1 ms
  • evidence ops › logs inspection findings with their catalog severity1 ms
  • evidence ops › adds and annotates a sample photo1 ms
  • evidence ops › replaces the signature with the latest capture1 ms
  • evidence ops › attaches a report and scanned equipment with feed events2 ms
  • evidence ops › records payment only where the industry takes payment1 ms
  • evidence ops › lets a sales rep complete with a no-order reason instead of an order1 ms
  • evidence ops › masks phone numbers and emails a visitor types before the note is stored1 ms
  • evidence ops › masks contact details in a typed transcript and cleans report text sent from a phone1 ms
  • evidence ops › appends notes4 ms
  • evidence ops › refuses a change that would push the job past 128 KB35 ms
  • op schema › accepts a well-formed op3 ms
  • op schema › rejects an unknown type0 ms
  • op schema › rejects an oversized note2 ms
  • op schema › rejects coordinates off the photo2 ms
  • op schema › rejects too many signature strokes2 ms
  • op schema › rejects a negative quantity1 ms
packages/core/test/catalog-match.test.ts6/6
  • reportToCatalogItems › maps the model's part names to catalog codes with their quantities5 ms
  • reportToCatalogItems › skips names that match nothing and merges duplicates1 ms
  • reportToCatalogItems › doesn't turn a single shared word into an expensive part1 ms
  • reportToCatalogItems › prefers the item the words point to when several share them1 ms
  • reportToCatalogItems › maps inspection findings to finding codes with the report's severity1 ms
  • reportToCatalogItems › uses a quantity of 1 when the model reports zero1 ms
packages/core/test/clock.test.ts10/10
  • demo clock › starts the demo day at 10:40 AM (minute 640)2 ms
  • demo clock › runs at 20x: one real minute is twenty demo minutes0 ms
  • demo clock › stops at 5:45 PM (minute 1065)0 ms
  • demo clock › never goes before the start, even when the device clock is behind0 ms
  • demo clock › returns whole minutes0 ms
  • demo clock › formats minute 640 as 10:40 AM0 ms
  • demo clock › formats minute 720 as 12:00 PM0 ms
  • demo clock › formats minute 1065 as 5:45 PM0 ms
  • demo clock › formats minute 420 as 7:00 AM0 ms
  • demo clock › formats minute 779 as 12:59 PM0 ms
packages/core/test/color.test.ts5/5
  • color math › computes WCAG contrast3 ms
  • color math › round-trips hex through OKLCH1 ms
  • color math › keeps out-of-gamut colors in sRGB by reducing chroma0 ms
  • color math › measures CIEDE2000 like the reference implementation0 ms
  • color math › accepts #RGB and #RRGGBB, rejects the rest1 ms
packages/core/test/fold.test.ts4/4
  • acknowledged changes on the phone › keeps showing them between the server's ack and the pull that carries them (no flicker back)8 ms
  • acknowledged changes on the phone › drops exactly the pushes a pulled snapshot already contains, so nothing is applied twice1 ms
  • acknowledged changes on the phone › keeps only what the server accepted: rejected and conflicting changes don't linger0 ms
  • acknowledged changes on the phone › re-applying a whole job's changes over a snapshot that has them is not safe, which is why they're dropped2 ms
packages/core/test/outbox.test.ts11/11
  • outbox › keeps ops in order and batches them under the byte limit3 ms
  • outbox › still sends an op that is bigger than the limit on its own1 ms
  • outbox › drops applied and duplicate ops, and resets the retry clock1 ms
  • outbox › drops a rejected op with a notice instead of blocking the queue1 ms
  • outbox › asks for a fresh snapshot after a conflict1 ms
  • outbox › backs off exponentially up to 30 seconds0 ms
  • outbox › namespaces storage by sandbox and generation0 ms
  • foldView › is the snapshot when nothing is pending3 ms
  • foldView › shows pending changes right away4 ms
  • foldView › does not mutate the snapshot2 ms
  • foldView › turns a pending op for a job that's gone into a notice1 ms
packages/core/test/packs.test.ts53/53
  • ships all four industries19 ms
  • hvac pack › has a crew of five with exactly one visitor2 ms
  • hvac pack › gives the visitor a short first stop: en route, two required items, no payment1 ms
  • hvac pack › puts the payment step on the visitor's following stop when the industry collects payment1 ms
  • hvac pack › references only existing customers, crew and checklist templates2 ms
  • hvac pack › keeps ids, job numbers and catalog codes unique0 ms
  • hvac pack › scripts one late job, one issue and one unassigned urgent job1 ms
  • hvac pack › has two voice samples whose scripted reports are valid7 ms
  • hvac pack › has two labels whose ground truth passes the format checks3 ms
  • hvac pack › has two sample photos and four coach steps per track1 ms
  • hvac pack › schedules everything inside the working day2 ms
  • hvac pack › scripts the simulated crew in order, only for non-visitor crew and existing jobs2 ms
  • hvac pack › contains no real URLs, real email domains or denied words26 ms
  • hvac pack › uses fictional 555-01xx phone numbers0 ms
  • pest pack › has a crew of five with exactly one visitor0 ms
  • pest pack › gives the visitor a short first stop: en route, two required items, no payment0 ms
  • pest pack › puts the payment step on the visitor's following stop when the industry collects payment0 ms
  • pest pack › references only existing customers, crew and checklist templates1 ms
  • pest pack › keeps ids, job numbers and catalog codes unique0 ms
  • pest pack › scripts one late job, one issue and one unassigned urgent job0 ms
  • pest pack › has two voice samples whose scripted reports are valid1 ms
  • pest pack › has two labels whose ground truth passes the format checks0 ms
  • pest pack › has two sample photos and four coach steps per track0 ms
  • pest pack › schedules everything inside the working day1 ms
  • pest pack › scripts the simulated crew in order, only for non-visitor crew and existing jobs1 ms
  • pest pack › contains no real URLs, real email domains or denied words15 ms
  • pest pack › uses fictional 555-01xx phone numbers0 ms
  • inspection pack › has a crew of five with exactly one visitor0 ms
  • inspection pack › gives the visitor a short first stop: en route, two required items, no payment0 ms
  • inspection pack › puts the payment step on the visitor's following stop when the industry collects payment0 ms
  • inspection pack › references only existing customers, crew and checklist templates1 ms
  • inspection pack › keeps ids, job numbers and catalog codes unique0 ms
  • inspection pack › scripts one late job, one issue and one unassigned urgent job0 ms
  • inspection pack › has two voice samples whose scripted reports are valid1 ms
  • inspection pack › has two labels whose ground truth passes the format checks1 ms
  • inspection pack › has two sample photos and four coach steps per track0 ms
  • inspection pack › schedules everything inside the working day1 ms
  • inspection pack › scripts the simulated crew in order, only for non-visitor crew and existing jobs1 ms
  • inspection pack › contains no real URLs, real email domains or denied words16 ms
  • inspection pack › uses fictional 555-01xx phone numbers0 ms
  • sales pack › has a crew of five with exactly one visitor0 ms
  • sales pack › gives the visitor a short first stop: en route, two required items, no payment0 ms
  • sales pack › puts the payment step on the visitor's following stop when the industry collects payment0 ms
  • sales pack › references only existing customers, crew and checklist templates0 ms
  • sales pack › keeps ids, job numbers and catalog codes unique0 ms
  • sales pack › scripts one late job, one issue and one unassigned urgent job0 ms
  • sales pack › has two voice samples whose scripted reports are valid0 ms
  • sales pack › has two labels whose ground truth passes the format checks0 ms
  • sales pack › has two sample photos and four coach steps per track0 ms
  • sales pack › schedules everything inside the working day0 ms
  • sales pack › scripts the simulated crew in order, only for non-visitor crew and existing jobs1 ms
  • sales pack › contains no real URLs, real email domains or denied words10 ms
  • sales pack › uses fictional 555-01xx phone numbers0 ms
packages/core/test/personalization.test.ts22/22
  • parseDemoParams › defaults to the HVAC technician demo without personalization3 ms
  • parseDemoParams › maps industry=PEST to pest0 ms
  • parseDemoParams › maps industry=inspection to inspection0 ms
  • parseDemoParams › maps industry=sales to sales0 ms
  • parseDemoParams › maps industry=plumbing to hvac0 ms
  • parseDemoParams › maps industry= to hvac0 ms
  • parseDemoParams › keeps a clean prospect name and derives a stable key2 ms
  • parseDemoParams › ignores an unsafe prospect name: <script>x</script>0 ms
  • parseDemoParams › ignores an unsafe prospect name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1 ms
  • parseDemoParams › ignores an unsafe prospect name: Acme 🔥1 ms
  • parseDemoParams › ignores an unsafe prospect name: Acme‮Heating0 ms
  • parseDemoParams › ignores an unsafe prospect name: https://evil.example0 ms
  • parseDemoParams › ignores an unsafe prospect name: fuck0 ms
  • parseDemoParams › normalizes valid accents and ignores invalid ones1 ms
  • parseDemoParams › reads the role and the link signature1 ms
  • parseDemoParams › accepts anything with get(), like URLSearchParams0 ms
  • denylist and link signatures › flags denied words case-insensitively, as whole words0 ms
  • denylist and link signatures › signs what the link personalizes0 ms
  • demoQuery › is empty for the default demo0 ms
  • demoQuery › carries the industry, prospect name, accent and signature forward0 ms
  • demoQuery › applies overrides and leaves the default role out0 ms
  • demoQuery › drops the signature when a signed field changes0 ms
packages/core/test/seed.test.ts25/25
  • seed for hvac › is deterministic19 ms
  • seed for hvac › names the company after the prospect when personalized1 ms
  • seed for hvac › starts every job at version 1 with stable ids1 ms
  • seed for hvac › gives finished jobs their evidence: signature or order, priced items, an annotated photo and a checklist summary2 ms
  • seed for hvac › leaves unfinished jobs without evidence2 ms
  • seed for hvac › stays small enough to ship to the phone1 ms
  • seed for pest › is deterministic3 ms
  • seed for pest › names the company after the prospect when personalized1 ms
  • seed for pest › starts every job at version 1 with stable ids1 ms
  • seed for pest › gives finished jobs their evidence: signature or order, priced items, an annotated photo and a checklist summary1 ms
  • seed for pest › leaves unfinished jobs without evidence3 ms
  • seed for pest › stays small enough to ship to the phone0 ms
  • seed for inspection › is deterministic3 ms
  • seed for inspection › names the company after the prospect when personalized0 ms
  • seed for inspection › starts every job at version 1 with stable ids0 ms
  • seed for inspection › gives finished jobs their evidence: signature or order, priced items, an annotated photo and a checklist summary1 ms
  • seed for inspection › leaves unfinished jobs without evidence1 ms
  • seed for inspection › stays small enough to ship to the phone0 ms
  • seed for sales › is deterministic2 ms
  • seed for sales › names the company after the prospect when personalized0 ms
  • seed for sales › starts every job at version 1 with stable ids0 ms
  • seed for sales › gives finished jobs their evidence: signature or order, priced items, an annotated photo and a checklist summary2 ms
  • seed for sales › leaves unfinished jobs without evidence16 ms
  • seed for sales › stays small enough to ship to the phone1 ms
  • jobs created by dispatch › builds an urgent job from a dispatch template1 ms
packages/core/test/signature.test.ts2/2
  • fitting a drawn signature into one request › leaves a normal signature untouched2 ms
  • fitting a drawn signature into one request › thins a long scribble to the limit, keeping whole points and each stroke's ends6 ms
packages/core/test/sim.test.ts11/11
  • simulated crew › has nothing due at the start of the demo day3 ms
  • simulated crew › returns the steps due so far, after the last one applied1 ms
  • simulated crew › completes a job the way a real crew member would2 ms
  • simulated crew › drives, arrives and resumes after an issue1 ms
  • simulated crew › skips a step when the office reassigned the job1 ms
  • simulated crew › skips a step that no longer applies1 ms
  • simulated crew › adds an order before completing a sales visit2 ms
  • board numbers › computes today's KPIs from the jobs2 ms
  • board numbers › reports same-day reports for inspections and orders for field sales27 ms
  • board numbers › flags running-late, issue and unassigned-urgent jobs2 ms
  • board numbers › does not call the visitor late before half an hour past the window start1 ms
API and databaseAPI, database and AI routes (PGlite)94/97 · 14 files
apps/web/test/budget.test.ts7/7
  • AI budgets › allows 20 AI calls per visitor per day and refuses the 21st2807 ms
  • AI budgets › never lets parallel requests exceed the global cap2148 ms
  • AI budgets › does not charge the visitor when the global cap refuses2286 ms
  • AI budgets › starts fresh on the next UTC day1713 ms
  • AI budgets › stops at the daily dollar budget and accounts for real spend2102 ms
  • AI budgets › caps transcriptions separately at 3 per visitor per day1842 ms
  • spend correction › never fails the request that already got its AI answer3 ms
apps/web/test/concurrency.neon.test.ts0/3
  • concurrency on real Postgres (Neon) › creates one sandbox when the same visitor asks ten times at once (skipped)0 ms
  • concurrency on real Postgres (Neon) › serializes parallel pushes to one sandbox into gapless, commit-ordered sequence numbers (skipped)0 ms
  • concurrency on real Postgres (Neon) › never lets parallel AI requests exceed the global cap (skipped)0 ms
apps/web/test/db-url.test.ts3/3
  • normalizeConnectionString › pins sslmode=require to verify-full (same behavior, no pg warning)2 ms
  • normalizeConnectionString › adds verify-full when sslmode is missing0 ms
  • normalizeConnectionString › leaves an explicit verify-full untouched0 ms
apps/web/test/db.test.ts4/4
  • database schema › applies all migrations to an empty database2577 ms
  • database schema › keeps one sandbox per visitor, industry and prospect name2186 ms
  • database schema › deletes everything a sandbox owns when the sandbox is deleted1524 ms
  • database schema › rejects a job that points at another sandbox's customer1445 ms
apps/web/test/label-scan.test.ts8/8
  • label scan › returns AI-read fields with format checks and the model id2785 ms
  • label scan › falls back to the scripted reading when the gateway fails2179 ms
  • label scan › labels a timeout as such1771 ms
  • label scan › uses the scripted reading without calling the model once the visitor's daily limit is reached2379 ms
  • label scan › refuses label ids that aren't part of the demo2146 ms
  • label scan — weak reads › falls back to the scripted reading when the model reads less than half the label1965 ms
  • label scan — weak reads › falls back when the model says the image isn't readable2470 ms
  • label scan — run log › still returns the reading when writing the run log fails2323 ms
apps/web/test/lock.test.ts2/2
  • sandbox lock › bounds how long a write can wait for the lock or run, on the server side2480 ms
  • sandbox lock › answers a lock or statement timeout as a retryable 503, not a 50026 ms
apps/web/test/office.test.ts13/13
  • board snapshot › includes KPIs, the attention strip and the feed3628 ms
  • simulated crew › materializes due steps once, as simulated crew events at their scripted times2074 ms
  • simulated crew › answers 'changed' when simulated crew moved even if the phone did nothing2112 ms
  • simulated crew › skips a step whose job the office reassigned2029 ms
  • dispatch › assigns the unassigned urgent job to the phone with a simulated push2454 ms
  • dispatch › makes an urgent job the phone's next stop, right after the stop in progress2232 ms
  • dispatch › creates an urgent job from a template for the phone2393 ms
  • dispatch › counts office writes toward the demo's daily quota, like phone changes1928 ms
  • dispatch › refuses to reassign a finished job and unknown crew1772 ms
  • job detail and customer report › returns a job with its customer, assignee and timeline1831 ms
  • job detail and customer report › answers 410 to a token from before a reset, like every other sandbox endpoint2033 ms
  • job detail and customer report › builds the customer email as 'would be sent' and logs it2494 ms
  • job detail and customer report › keeps logs to this visitor's demo2091 ms
apps/web/test/payments-cron.test.ts11/11
  • payments (test mode) › runs simulated by default: server-computed amount, a simulated webhook, no card data3671 ms
  • payments (test mode) › refuses payment for jobs that don't take payment here1918 ms
  • payments (test mode) › uses Stripe's test API only with a test key1868 ms
  • payments (test mode) › creates a Stripe test PaymentIntent when a test key is configured2123 ms
  • payments (test mode) › a double tap or a retried request records one payment and answers the same2203 ms
  • payments (test mode) › a retried Stripe request that gets the same PaymentIntent back succeeds without a second record2375 ms
  • Stripe webhook › does not exist while payments are simulated1877 ms
  • Stripe webhook › verifies the signature, rejects live events and ignores duplicates1996 ms
  • Stripe webhook › accepts any of several v1 signatures, as Stripe sends while a secret is rotated2012 ms
  • cleanup cron › fails closed without the right secret, including when none is configured1954 ms
  • cleanup cron › deletes sandboxes idle for 24 hours and runs once per hour1859 ms
apps/web/test/robots.test.ts5/5
  • robotsHeader › keeps the landing and Under the hood indexable2 ms
  • robotsHeader › noindexes any page opened from a proposal link0 ms
  • robotsHeader › always noindexes the app surfaces0 ms
  • robotsHeader › treats an empty for= like a proposal link0 ms
  • needsVisitorCookie › is only for the pages that open a private demo0 ms
apps/web/test/session.test.ts11/11
  • POST /api/session › works without a cookie and sets one3535 ms
  • POST /api/session › returns the same sandbox for the same visitor, even under parallel requests1932 ms
  • POST /api/session › keeps a separate sandbox per industry and per prospect1964 ms
  • POST /api/session › marks proposal links signed by the link builder, and ignores bad signatures2263 ms
  • POST /api/session › limits new sandboxes per visitor per day2091 ms
  • POST /api/session › goes into capacity mode at the global cap, except for signed proposal links2341 ms
  • POST /api/session › when the database is near its size budget, refuses new demos but keeps existing ones2226 ms
  • session requests per network › slows down a burst from one address without storing it, and leaves other addresses alone2223 ms
  • reset › reseeds with a new generation and a fresh clock; old phone tokens get 4102108 ms
  • reset › allows 20 resets per sandbox per day2878 ms
  • reset › refuses phone tokens2034 ms
apps/web/test/skeleton.test.ts3/3
  • walking skeleton: phone → API → board › shows a status change made on the phone on the office board2608 ms
  • walking skeleton: phone → API → board › answers 'unchanged' when nothing happened since the board's last read2151 ms
  • walking skeleton: phone → API → board › gives the phone its day on first pull and only changes afterwards2141 ms
apps/web/test/sync.test.ts10/10
  • push › applies a batch in order with gapless sequence numbers3818 ms
  • push › answers a replayed batch with the original results and changes nothing1948 ms
  • push › rejects an op id reused with different contents2125 ms
  • push › rejects ops for unknown jobs and malformed ops without blocking the rest2439 ms
  • push › enforces the daily change quota per sandbox2095 ms
  • push › refuses request bodies over 64 KB2250 ms
  • pull › returns every job changed since the cursor, including one reassigned away from the phone1992 ms
  • isolation between visitors › never lets one visitor's changes reach another visitor's demo2276 ms
  • isolation between visitors › keeps the office board behind the owner token1860 ms
  • isolation between visitors › returns 404 for a token whose sandbox is gone2380 ms
apps/web/test/tokens.test.ts8/8
  • session tokens › round-trips signed claims4 ms
  • session tokens › rejects a tampered payload1 ms
  • session tokens › rejects a tampered signature0 ms
  • session tokens › rejects an expired token0 ms
  • session tokens › rejects an unknown key id1 ms
  • session tokens › keeps accepting tokens signed with the previous key after rotation1 ms
  • session tokens › rejects malformed tokens without throwing1 ms
  • session tokens › refuses secrets shorter than 32 bytes1 ms
apps/web/test/voice.test.ts9/9
  • transcription › transcribes the sample recording live when the budget allows2828 ms
  • transcription › uses the cached machine transcript after 3 live transcriptions, without calling the model2016 ms
  • transcription › falls back to the cached transcript when transcription fails1859 ms
  • transcription › refuses samples that aren't part of this industry's demo1837 ms
  • voice note → structured report › returns the model's report, cleaned of links1875 ms
  • voice note → structured report › uses the sample's scripted report when the model output is unusable1801 ms
  • voice note → structured report › labels a timeout2249 ms
  • voice note → structured report › redacts contact details from typed notes before the model sees them2172 ms
  • voice note → structured report › falls back to the rule-based extractor for typed notes when AI is over budget2154 ms

Real vs simulated

What you can trust in this demo, and what is standing in for a service a real deployment would call.

Real and simulated parts of the demo
PartStatusWhat that means
The phone appRealThe Expo / React Native app, built for the web from the same code as the iOS and Android builds.
API and databaseRealNext.js route handlers on Vercel and Postgres on Neon; your changes are stored and synced.
Offline queue and syncRealChanges are saved on the phone, sent in order when there's signal, applied exactly once.
AI (voice, report, label)Real, cappedReal model calls through the Vercel AI Gateway, with per-visitor and daily limits; labeled fallbacks when capped.
Office boardRealReads the same database; your phone's changes appear as yours.
Other crew membersSimulatedScripted by the demo clock and labeled as simulated on the board.
Push notificationsSimulatedShown in the phone's inbox as 'Simulated push'; no device service is called.
Texts and emailsSimulatedBuilt and logged as 'would be sent'; nothing leaves the demo.
WebhooksSimulatedSigned payloads logged against reserved .invalid addresses.
PaymentsTest modeSimulated by default; Stripe's test mode when test keys are configured. No real charges.
Company, people, placesFictionalMarlow Bay and everyone in it are made up; emails use example.com.

What production adds

The concept is built so these slot in without rewriting the app: effects already go through one log, and the API already separates the office from the phone.

Sign-in and roles
Company accounts, technician and office logins, per-role permissions.
Real-time channel
Server-sent events or a realtime service instead of polling, for instant board updates at scale.
Real push, SMS and email
APNs / FCM through Expo push, a texting provider and transactional email, behind the same effect log.
Store builds
EAS builds for the App Store and Google Play, over-the-air updates, crash reporting.
Location and routing
Background location with consent, real maps and turn-by-turn hand-off, route optimization.
Integrations
Accounting and CRM sync (invoices, customers, parts), and inventory for the parts catalog.