613
613parts an APC associate store
DRIVER APP · TABLET PWA · v0.5.0
Tablet PWA · Offline-first

a tool the truck
actually needs.

10" tablet mounted in each delivery truck. Driver enters PIN at 8am, today's route loads in 2 seconds, app works for the entire shift even in rural dead zones, syncs deliveries back when signal returns. ~1 week to build, ~$720 in hardware, $0/mo to run.

Form factor
10" tablet
Connectivity
Offline-first
Auth
4-digit PIN
Trucks
2 (BV + KGN)
Hardware
~$720
Hosting
$0/mo
The 5 screens

five taps from "good morning" to "delivered."

Big tap targets (56px+ minimum), high contrast, no animations, single column on the left half / actions on the right. Designed to be useable in winter gloves at -20°C with the heater blasting and snow on the windshield.

← BACK
Himmat
BELLEVILLE
enter your PIN
4 digits
1
2
3
4
5
6
7
8
9
0
SCREEN 01
PIN login
Pick your name (4-6 drivers), enter 4-digit PIN. Takes ~8 seconds total. JWT issued, valid 24h. Re-PIN every morning.
Himmat
BELLEVILLE TRUCK · 2026-05-04
● online
END DAY
9
Stops
3
Done
0
Exception
6
Pending
1
Marcus T.
23 Main St, Belleville K8N 4Z2
#613-2026-04832 · $264 · drop $15
2
Sarah K.
456 Bridge St, Belleville K8N 5K1
#613-2026-04855 · $1,489 · free drop
3
Dave R.
12 Highway 401 W, Trenton K8V 1W7
#613-2026-04901 · $89 · pickup BV
4
Lisa P.
88 Front St, Belleville K8N 2Y4
#613-2026-04912 · $389 · free drop
SCREEN 02
today's route
9 stops in optimized order. Big stat bar shows progress at a glance. Tap any stop to view details and act on it. Online/offline pill in top-right.
← ROUTE
STOP 3 OF 9
Order #613-2026-04901
Dave R.
12 Highway 401 W, Trenton K8V 1W7
📞
Call
613-555-1234
🗺️
Navigate
Open Maps
4× ACDelco brake padsACD-17D1414
Paid via Stripe $89.00
✓ MARK DELIVERED
! UNDELIVERABLE
SCREEN 03
stop detail
Customer name big, address big. Tap-to-call opens dialer. Tap-to-navigate opens Maps with destination pre-loaded. Order paid via Stripe — no collection at door. Two giant action buttons.
← BACK
MARK DELIVERED
Dave R.
📷
Tap to take photo
left at side door per customer request
✓ CONFIRM DELIVERED
SCREEN 04
delivery complete
Photo from camera (optional), signature on canvas (optional), free-text notes (optional). Per your call — none required. Driver can just tap CONFIRM and move on.
← ROUTE
2026-05-04
end of day · belleville
8
Delivered
1
Exceptions
$3,142
Revenue
0
Unsynced
all synced
END DAY · LOG OUT
SCREEN 05
end of day
Big stats. Sync confirmation. If anything's still queued, app forces a sync attempt before allowing logout. Logout clears today's route + signs out. Tomorrow morning starts fresh.
DEFAULT BEHAVIOR
photo + signature optional

Per your call — driver can just tap CONFIRM DELIVERED with no photo, no signature, no notes. Default flow takes ~3 seconds per stop. Capture only when something's worth recording (damaged box, weird location, customer drama).

EXCEPTION FLOW
undeliverable in one tap

5 reasons preset (no one home / wrong address / refused / damaged / other) plus optional notes. Stop becomes red on the route screen. Counter staff calls customer to reschedule.

Offline-first architecture

drives in dead zones. syncs when ready.

Service worker caches the app shell aggressively. Today's route is loaded once at 8am, cached in IndexedDB. Every delivery write hits IndexedDB first, then attempts a server POST. If POST fails, the record stays in the queue with a retry timestamp. When connectivity returns, Background Sync API drains the queue automatically.

8AM
PWA · ONLINE
Driver enters PIN
JWT issued · stored in IndexedDB
SERVER → R2
GET /route/today
Reads deliveries/2026-05-04/belleville.json
CACHED
Route in IndexedDB
Service worker caches HTTP response too
10AM
PWA · OFFLINE
No cell signal
Driver in rural Hastings · between cell towers
QUEUE
Tap "Mark Delivered"
Record written to IndexedDB · synced=false
10:15
PWA · OFFLINE
Stop 4, 5, 6 done
3 records queued · UI shows "● offline"
QUEUE GROWING
4 unsynced
Service worker waits for connectivity
11AM
PWA · BACK ONLINE
Cell signal returns
Driver back near Belleville · "● online"
SYNC FIRES
Background Sync API
Drains queue · 4 POSTs to /complete
SERVER → R2
delivery-events/ written
Photos to delivery-photos/ if present
5PM
PWA · END OF DAY
All synced
Driver taps END DAY → logs out
Backend extensions

four new endpoints. one new auth helper.

All added to the existing 613parts-bot-worker. JWT-based auth (HS256, 24h tokens) using the Web Crypto API — no external dependencies. Driver list is a static file in the repo with salted PIN hashes.

POST/api/v1/driver/login

Driver ID + PIN → JWT (24h). Returns driver info for client-side display. PINs are salted SHA-256, never stored raw.

Body: {driver_id, pin} Returns: {token, driver, expires_in}
GET/api/v1/driver/list

Returns the active driver list (id, name, branch). Used by the PIN screen dropdown. No auth required so the login screen can show drivers before they're authenticated.

Returns: {drivers: [{id, name, branch}]}
GET/api/v1/driver/route/today

Today's stops for the authenticated driver's branch. Reads deliveries/{date}/{branch}.json from R2. Service worker caches the response for offline use.

Auth: Bearer JWT Returns: {date, branch, driver, stops: RouteStop[]}
POST/api/v1/driver/delivery/:order_id/complete

Marks an order delivered. Accepts optional photo (base64 JPEG → R2) + signature SVG + notes. Writes a delivery event JSON to R2.

Auth: Bearer JWT Body: {delivered_at, photo_base64?, signature_svg?, notes?}
POST/api/v1/driver/delivery/:order_id/exception

Marks an order undelivered with a reason. Counter staff calls customer to reschedule.

Auth: Bearer JWT Body: {reason, attempted_at, notes?}
Sample code · auth.ts

jwt with web crypto api. no deps.

Cloudflare Workers ship with the full Web Crypto API, so JWT signing + verification + PIN hashing all use built-ins. Zero npm packages added for auth.

worker/src/lib/auth.ts
excerpt · HS256 signing
export async function signJWT(payload: Omit<JWTPayload, 'iat' | 'exp'>, secret: string): Promise<string> {
  const now = Math.floor(Date.now() / 1000);
  const fullPayload: JWTPayload = { ...payload, iat: now, exp: now + TOKEN_TTL_SECONDS };

  const headerB64  = base64urlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
  const payloadB64 = base64urlEncode(JSON.stringify(fullPayload));
  const signingInput = `${headerB64}.${payloadB64}`;

  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput));
  return `${signingInput}.${base64urlEncode(new Uint8Array(signature))}`;
}

export async function verifyPIN(pin: string, salt: string, hash: string): Promise<boolean> {
  const computed = await hashPIN(pin, salt);
  // Constant-time comparison
  if (computed.length !== hash.length) return false;
  let mismatch = 0;
  for (let i = 0; i < computed.length; i++) {
    mismatch |= computed.charCodeAt(i) ^ hash.charCodeAt(i);
  }
  return mismatch === 0;
}
Project structure

two repos. shared backend.

Worker gets new auth + driver handler files. New separate repo driver-app houses the PWA — Vite + React + Tailwind. Both deploy to Cloudflare (Workers + Pages).

worker/ # existing bot worker · gets driver extensions └── src/ ├── handlers/ │ ├── search.ts │ ├── fulfillment.ts │ ├── generator.ts │ └── driver.ts NEW# 5 endpoints in one file ├── lib/ │ ├── ... (existing libs) │ ├── auth.ts NEW# JWT + PIN hashing · Web Crypto │ └── drivers.ts NEW# Driver registry (Himmat, Preet, etc.) ├── index.ts # EDIT · adds /api/v1/driver/* routing └── types.ts # EDIT · adds JWT_SECRET to Env driver-app/ # NEW · the tablet PWA ├── package.json # React + Vite + Tailwind + idb-keyval ├── vite.config.ts # vite-plugin-pwa for offline shell + manifest ├── tailwind.config.js # 613parts brand colors + tap-target sizes ├── tsconfig.json ├── index.html └── src/ ├── main.tsx # React mount ├── App.tsx # Hash-based router ├── types.ts ├── styles.css # Tailwind directives ├── screens/ │ ├── LoginScreen.tsx # PIN pad + driver picker │ ├── RouteScreen.tsx # Today's stops + sync status │ ├── StopDetailScreen.tsx # Single stop with action buttons │ ├── DeliveryCompleteScreen.tsx # Photo + signature + notes │ └── EndOfDayScreen.tsx # Stats + sync confirmation + logout └── lib/ ├── api.ts # Fetch wrappers with JWT ├── storage.ts # IndexedDB via idb-keyval └── sync.ts # Queue drainer + Background Sync API
3 new files in worker 15 new files in driver-app ~1,900 lines 2 new deps · react + idb-keyval
Hardware spec

$720 once. lasts 3 years.

2 trucks × (1 tablet + 1 mount) + cabling. Tablets are kiosk-locked to the app, hardwired to truck power, theft-tethered with a steel cable lock through the dashboard mount.

Samsung Galaxy Tab A9+

~$280 CAD × 2 = $560

11" screen, 1920×1200, Android 14 with Chrome (full PWA support). 2-day battery if needed for backup. Built-in WiFi + LTE option (~$100 more for cellular variant — recommend cellular so the truck doesn't depend on the customer's WiFi).

RAM Mounts X-Grip

~$80 CAD × 2 = $160

Twist-lock dashboard mount with X-Grip cradle for 7-13" tablets. Holds tablet upright in landscape mode. Suction or screw-in base depending on truck dashboard type.

Hardwired power

included w/ tablet

USB-C cable run from fuse box to mount. Tablet always charging when truck is on. No "forgot to plug in" risk.

Theft tether

~$20 CAD × 2 = $40

Steel cable lock through mount and dashboard frame. Makes tablet a 30-second-with-bolt-cutters problem instead of a 3-second grab.

Deployment

six steps to two trucks live.

Backend extensions deploy to the existing Worker. Frontend deploys to Cloudflare Pages. Hardware setup takes ~30 min per tablet.

01 · BACKEND

Deploy Worker extensions

Set the JWT secret. Existing Worker accepts new routes.

cd worker
npx wrangler secret put JWT_SECRET
npm run deploy
02 · DRIVER PINS

Generate real PIN hashes

Replace the dev PINs in src/lib/drivers.ts with real ones.

node -e 'const c=require("crypto");
const salt=c.randomBytes(8).toString("hex");
const pin="REAL_PIN_HERE";
console.log({salt, hash: c.createHash("sha256")
  .update(salt+pin).digest("hex")})'
03 · FRONTEND

Build & deploy PWA

Cloudflare Pages with custom domain.

cd driver-app
npm install
npm run build
npx wrangler pages deploy dist \
  --project-name=613parts-driver-app
04 · DOMAIN

Wire drive.613parts.ca

In Cloudflare DNS: CNAME drive → pages.dev. SSL automatic.

# In Cloudflare dashboard:
# DNS → Add record →
# Type: CNAME
# Name: drive
# Target: 613parts-driver-app.pages.dev
05 · TABLETS

Provision both tablets

Setup Wizard → connect WiFi → open Chrome → drive.613parts.ca → Add to Home Screen → Kiosk Mode.

# 30-min checklist per tablet:
# 1. Install on truck mount
# 2. Wire to fuse box (always-on USB-C)
# 3. Connect to truck WiFi or cellular SIM
# 4. Install PWA
# 5. Enable Knox Kiosk Mode (Samsung)
# 6. Test PIN login + offline mode
06 · TRAIN

Soft launch · 1 week

Drivers run BOTH the printed sheet AND the tablet in parallel. Confirm parity. Drop the printed sheet at end of week 1.

# Week 1: parallel run
# Week 2: tablet only, paper backup
# Week 3: tablet exclusive
# Add new features as drivers ask