A full account of the workshop management system now running Storm Rides' job pipeline — its feature set, its architecture, the AppSheet system it replaced, and a cost-benefit analysis grounded in the shop's own production data rather than projections.
STRM 2.0 replaced Storm Rides' AppSheet/Google Sheets setup with a purpose-built web app: one job pipeline, one parts catalog, one customer database, live Shopify invoicing, scheduling, printing and payments, each staff member signed in as themselves. It was designed and directed solo, mostly on unpaid overtime, and has been carrying the shop's live job board since 15 July 2026 — see §02 for who built it and how.
This wasn't commissioned. I'm a Bachelor of Media Design student, not a software engineer — but I've been involved in this shop's day-to-day processes for close to four years, long enough to build up a running list of exactly where the old AppSheet system was getting in the way. That list is the actual origin of this project: not a brief handed to me, but friction I'd been feeling myself, workflow by workflow, for years.
How it was actually built: the ~45,500 lines of code weren't hand-written by me — they were written by directing Claude Code, Anthropic's AI coding tool, across 39 releases of iteration. What I brought was everything a contractor would otherwise have had to spend weeks learning from scratch: what the ticket pipeline actually needed to look like, how warranty claims really get handled at the counter, what a technician's day requires, and every architecture and feature call across those 39 releases. I tested every release against real shop workflows, which is why 33 of the 35 issues I filed against it were closed within days of go-live. Claude Code wrote the code; I decided what to build, in what order, and whether it was actually right. That discipline didn't stop at go-live: building the Analytics dashboard (§04) the same way caught three real issues before any of them reached production — a client-side bundle accidentally pulling in the database driver, a staff-activity endpoint locked to a stricter role than its new home needed, and a permission check that had to grant one admin page to a restricted account without quietly opening the door to user management and data recovery alongside it. Each fix was confirmed against the running system before being marked done.
The bet: I put my own money behind it before asking for anyone else's. I self-funded roughly $180 NZD to get the project to a working MVP — enough to demonstrate it was real and worth a proper look — before pitching it for approval. That pitch was approved and the $180 was reimbursed; everything built since then (the AppSheet migration, go-live, and the releases that followed) happened on infrastructure the business now owns outright.
STRM 2.0 is a web app (desktop and mobile) covering the full life of a job: a customer drops off a scooter, it's ticketed, quoted, worked on, invoiced through Shopify, and handed back — with every step, comment, and dollar attributed to the staff member who did it. It runs on Cloudflare's global network with a managed Postgres database, and is reachable from any device with a browser, not just the one computer the spreadsheet used to live on.
Each ServiceTicket carries a sequential job number, a customer, one or more scooters
(multi-scooter jobs are a single ticket with per-scooter parts and comments), a status across a 12-value
pipeline grouped into 5 visible stages — Quoting → Approval → Working → Ready → Collected — and a full
event timeline. Nothing is a static row in a sheet; every change is a recorded, attributed event.
Four roles — Admin, Technician, Developer, and a restricted "Limited" tier with per-page view/edit grants (e.g. the accountant sees Roster only) — replace the single shared AppSheet login the shop had been using to avoid per-seat licensing. See §08 for what that workaround was actually costing.
Grouped by the part of the shop's day it covers. All of the below is live in production, not a roadmap.
Quote → charge line states, partial-quantity approval (e.g. raising labour from 1 to 1.5 hours only charges the approved portion), technician notes that print straight onto invoices, and a searchable Service View grouping active jobs, customers, and completed/cancelled history.
3,490 customers and 2,239 scooters, deduplicated by name/email/phone, with full service history per machine and a "rebooked" link when a completed job comes back in.
Capacity = active technicians × a configurable daily budget (4 hrs/tech by default). Over-budget days flag automatically; jobs left un-started auto-nag, then auto-move to the next working day.
Public /book page, no login: pick a scooter, describe the issue, choose an open
weekday slot. Matches against existing customer records without ever leaking one customer's details
into another's.
519-item parts catalog; charged lines become a Shopify draft or finalized order directly from the ticket — 8 real orders created this way since go-live, no re-keying into a second system.
A YES/PARTIAL warranty claim auto-applies a 100%/50% discount to the Shopify order, linked back to the customer's original purchase picked from their real Shopify order history — no manual discount math.
QR job labels and status-correct documents (quote / warranty log / receipt / invoice) print through a small Windows helper that connects out to the app — any signed-in device, anywhere, can trigger a print at the shop without opening a port on the shop network.
A second outbound-only Windows bridge lets staff start a Purchase transaction on the front-desk Windcave terminal directly from a ticket, with live status (Awaiting Card → Accepted/Declined) streamed to every staff member watching that job.
A Cloudflare Durable Object pushes live updates to every open tab — roster, job pages, and the schedule board refresh themselves. No "someone else has this job open" stale-data conflicts.
A standing weekly pattern plus per-week overrides, rotating shift cycles, leave tracking, and a draft → publish workflow so a roster can be edited without going live mid-change.
Scan a printed label to jump straight to its job, or run an "Audit" pass — pick an expected status and scan every scooter in that area, with a live count and a red flag on anything in the wrong place.
Each ticket has an unguessable link the customer can use to check status, approve a quote, update their own contact details, or leave a note — without a phone call either way.
Deleted jobs sit in Recovery for 7 days before permanent purge. A developer metrics page tracks logins and adoption; a bug-report button feeds a triage queue admins can resolve in-app.
Revenue, jobs booked, jobs completed, returning-customer rate, and hours tracked — five tiles with sparklines and period-over-period deltas, one date-range picker driving both a job-pipeline funnel and a top-parts-by-revenue breakdown. The same bar sits above the ticket table and on its own dedicated page — and is now grantable to a Limited account (e.g. the accountant) without opening user management or data recovery alongside it.
The stack was chosen to minimise ongoing cost and maintenance burden for a single-developer system, while still supporting realtime multi-device use, payments hardware, and a public customer-facing surface.
| Layer | Technology | Why |
|---|---|---|
| Application | Next.js (App Router) + TypeScript, Tailwind, shadcn/ui | Single codebase for UI, API routes, and server logic; strong typing across 318 files. |
| Hosting | Cloudflare Workers via OpenNext | Global edge network, no server to patch or reboot; ~$5/mo Workers Paid plan covers the shop's whole load. |
| Realtime | Durable Objects (one hibernating room per channel) | Live roster/job/schedule updates and the label + EFTPOS bridges, at near-zero idle cost — rooms sleep when no one's connected. |
| Database | PostgreSQL (Neon, ap-southeast-2) + Prisma ORM | 25 relational models, 40 migrations, transactional integrity for money-handling operations. Free tier at current scale. |
| Auth | NextAuth v5 — email+password, and Employee ID + PIN for the shared shop terminal | Individual accountability without slowing down quick tech switching on the workshop floor. |
| Commerce | Shopify Admin GraphQL API | Invoicing and warranty claims write directly into the shop's existing Shopify store — no second ledger. |
| Resend HTTP API | Transactional booking/password-reset/portal email; works on Workers' edge runtime (no SMTP). | |
| Hardware bridges | Two custom Windows tray apps (label printer, EFTPOS terminal) | Outbound-only WebSocket connections — no open ports on the shop network, works from any device anywhere. |
| CI/CD | GitHub Actions → Cloudflare Workers on every push to main | Migrations and deploys are automatic and repeatable, not a manual checklist. |
Scale, in numbers: 25 database models, 12 enums, 40 applied migrations, 98 API endpoints, 318 TypeScript/TSX source files, ~45,500 lines of application code. Security posture includes role-based access control enforced at the middleware layer (pages and API methods), soft-delete with a 7-day recovery window on destructive actions, brute-force lockout on PIN login, and a full immutable audit-event log distinct from human comments.
Everything about how this is built — serverless compute, a managed database that scales with usage instead of a fixed server that has to be pre-sized, integrations added as independent bridges rather than rewrites — means growth doesn't carry the cost most software growth stories assume. The architecture has room to grow into a much bigger shop, and into systems well beyond the ones it already talks to.
What's actually assumed to scale here: job/data volume — not headcount, and not logins. Cloudflare's and Neon's costs are driven by requests and compute, which track how many people are actively using the system and how often, not how many records exist. This model assumes a bigger shop processes more jobs per person rather than hiring proportionally for every unit of growth — the same way it already runs today — so request volume (and the cost that follows it) grows much more slowly than raw job count. If staff headcount scaled 1:1 with job volume instead, the cost side would move up faster than shown here, though still nowhere near revenue at these dollar amounts.
Three real systems are already wired in, each added without touching the others: Shopify (commerce, direct API), a Windows label printer (hardware, outbound bridge), and a Windcave EFTPOS terminal (payments, the same outbound-bridge pattern again). Same shape both times — a small standalone bridge that dials out to STRM, rather than STRM reaching into shop hardware. Whatever plugs in next follows the same playbook: an API where one exists, an outbound bridge where it doesn't.
The owners have mentioned wanting STRM to work with WildJar, the shop's inbound call-tracking system. Phone numbers are already stored digits-only and matched the same way everywhere in STRM, which is exactly the join key a call-tracking integration would need — a call comes in, WildJar identifies the number, STRM matches it to the customer record automatically. Nothing's scoped or built yet; this is a real example of the kind of ask this architecture is set up to say yes to, not a promise of a specific delivery date.
Nothing was thrown away. Legacy AppSheet/Google Sheets exports (jobs, parts logs, notes, technician time) were imported by a purpose-built, non-destructive script that resolves broken legacy reference codes, recovers technician timer data the original export never captured cleanly, and de-duplicates against whatever had already been entered natively. It was run twice against the still-live AppSheet system to reconcile the new database ahead of time, then a final pass on 15 July 2026 completed the switch-over.
AppSheet charges per user. To avoid paying for the whole team, the shop ran on three shared logins for a staff of up to a dozen people. That workaround kept the subscription bill down, but it quietly gave up almost everything a multi-user system is supposed to provide:
Under the shared-login workaround: attribution existed only as a manual convention, not a system guarantee — staff picked their own name from a dropdown when logging an action (e.g. writing a comment after calling a customer), but nothing enforced it. A skipped step, a wrong name picked, or an edit left unattributed altogether was invisible to the system, and there was no login-level record of who was actually behind the screen at the time regardless of what the dropdown said. No way to give the accountant read-only access without also giving them edit access to jobs. No individual technician time tracking, so labour hours and productivity were never separable per person. No personal state — draft notes, saved filters, notifications — because "the account" wasn't really any one person. And a single leaked or forgotten password sat behind everyone's work at once.
Under STRM 2.0: 8 staff members sign in as themselves today (12 accounts total across Admin/Technician/Developer/Limited roles). Every status change, part added, quote approved, or order created is attributed automatically to the real, authenticated person who did it, in an immutable event log — no dropdown to remember, nothing to fake or skip (357 such events logged in the first four business days alone — see §09). The accountant's account is scoped to exactly the views they need. Technicians get individual timers feeding a leaderboard. A PIN-based quick-switch keeps the shared shop terminal fast without going back to a shared identity.
| Capability | AppSheet (as run) | STRM 2.0 |
|---|---|---|
| User accounts | 3 shared logins for the whole team | Individual login per staff member, PIN quick-switch on the shop terminal |
| Who did what | Self-reported per action (a name dropdown), unenforced | Automatic, tied to a real login, permanently |
| Permissions | All-or-nothing per shared account | 4 roles + granular per-page view/edit grants |
| Realtime multi-device sync | Sheet-style refresh/locking friction | Live push updates via a dedicated realtime layer |
| Shopify invoicing | Manual, separate step | Draft/final orders created directly from a ticket |
| Warranty discounting | Manual order discount math | Automatic 100%/50% discount, linked to the original order |
| Customer self-service | None — phone/email only | Self-service portal link: status, approvals, contact updates |
| Online booking | None | Public booking page with live capacity |
| Label & EFTPOS integration | Manual, device-bound | Print/charge from any signed-in device via outbound bridges |
| Deleted data | Typically immediate, unrecoverable | 7-day recovery window before permanent purge |
| Extensibility | Bounded by AppSheet's platform | Fully custom — new features ship as needed (39 releases in 17 days) |
| Monthly cost | $30 USD (workaround) / ~$80 USD if licensed for real headcount | ~$5 USD, all-in |
Two honest categories here, kept separate throughout: inherited history — 4.5 years of real AppSheet-era job data, now unified into one system — and native activity — what's happened since STRM 2.0 itself went live on 15 July. The system has been carrying live production traffic for four business days at the time of writing; the numbers below are early, not mature.
3 on cutover day, 7 the next day, 6 the day after — created directly in the new system rather than imported, in the first five calendar days.
1 draft + 7 finalized Shopify orders created directly from tickets since go-live — invoicing with zero re-keying into a second system.
28 filed in a single concentrated testing session on cutover day; 94% were resolved within days — a real-time bug bar, not a support backlog.
Building the Analytics dashboard surfaced a still-active job (Roger Daniel) with an entry date stuck in 2022 from the original import — invisible in a spreadsheet, obvious the moment it showed up alongside real, current jobs. Corrected in the local database; the same fix against production is still pending.
AppSheet is fully sunsetted: billing cancelled, no longer the operational system in any capacity. The original app stays reachable, but only as a read-only reference for cross-checking a handful of very old jobs where the initial data port didn't carry across cleanly — not a parallel system, and no ongoing cost. The $30/month figure below is what the shop no longer pays.
$30 → $0 per month: AppSheet's subscription is cancelled, not just superseded by daily use of STRM.
STRM's own ~$5/month hosting is the only line item left, so this ~$300/year swing
against the old workaround is already realized.
$80 → ~$5 per month = ~$900/year versus what AppSheet would have cost to run
honestly, with one login per person, instead of the 3-account workaround.
Reconstructed from commit timestamps: 28 distinct working sessions across 12 active days within a 17-day span, clustering to an estimated 40+ hours of directing and testing the build — almost certainly more once design thinking and review between commits is counted. Some of that time fell inside working hours; most was unpaid overtime, on top of regular duties. $180 NZD of that investment was also personal money, risked before there was any approval or guarantee of reimbursement (see §02).
Market reference point (not an actual quote, and not a like-for-like one): a NZ software contractor or agency building equivalent bespoke workshop-management software — ticket pipeline, RBAC, realtime sync, Shopify integration, custom payment-terminal and label-printer bridges, and a public booking/self-service portal — would typically be quoted in the $15,000–$40,000+ NZD range for the build alone, before ongoing hosting, and before the AppSheet migration and data-cleanup work this project also did. This is a benchmark for the scope of what got built, not a claim that directing an AI is equivalent to hiring a contractor. A contractor's price also buys accountability — someone to hold to a warranty, someone to call if it breaks — and that doesn't transfer the same way when the code is AI-written. That's a real difference, not a footnote.
Payback: because the ongoing infrastructure cost (~$5/mo) is already below what was being paid for the old system (~$30/mo), the hosting cost alone pays for itself immediately — every month running STRM 2.0 costs less than the AppSheet subscription it replaced, on day one, regardless of how the build time itself is valued. The larger return is operational: per-user accountability, an audit trail that didn't previously exist, integrated payments and invoicing, and a customer self-service surface — value AppSheet's platform was not going to deliver at any price point without a rebuild of its own.
Ongoing development cost: the ~$5/month figure above is hosting only. Any future feature work beyond what's already shipped — the "can we also add X" requests that come up once a system like this is in daily use — depends on continued access to an AI coding subscription (Claude) to keep building at anything like this pace. Storm Rides already plans to get one regardless of this report, so it isn't a new cost — it's the real ongoing cost of extending this system, separate from hosting. Claude subscriptions start at roughly US$20/month (Claude Pro), scaling up for heavier use — a small fraction of a single hour of contractor time.
Every usage figure in §09 will firm up over the coming weeks. Treat the login ramp and native-ticket counts as an early trend, not a settled baseline.
The code wasn't hand-built from one person's tacit knowledge — it was written by Claude Code and documented for exactly this handoff scenario. See §11 for what actually would, and wouldn't, be a problem if I stepped away.
All 519 parts currently come from the imported legacy price list; none are yet linked to a Shopify variant ID, so price/stock sync from Shopify — built and available — hasn't been switched on for the catalog yet.
The booking portal has taken 1 request and the EFTPOS bridge just shipped; both are freshly live features whose adoption will build over the coming weeks, not yet a mature channel.
Email itself is fully sorted — a verified sending domain, live since v1.0.0 (noreply@service.stormrides.nz, replies routed to support@stormrides.nz). The real open item is DNS: stormrides.nz isn't hosted on Cloudflare, so the app can't claim service.stormrides.nz as its own hostname yet and sits on strmservice2.stormrides.workers.dev instead.
Insurance-claim quotes are still handled outside the normal ticket flow — there's no workflow that produces the format an insurer expects or tracks a claim distinctly from a regular customer job.
Servicing the shop's own demo/showroom stock currently runs through the same pipeline as real customer jobs, under a generic "Storm Rides" customer record — no separate pool, so it can collide with genuine customer bookings, and technician time on this work needs to keep counting toward their output rather than disappearing into an edge case.
Move stormrides.nz's DNS to Cloudflare for a proper hostname, run the Shopify parts sync to link the catalog live, build an insurance-quoting workflow and a separate shop-owned/demo job pool, and let 30–60 more days of production data mature before revisiting these figures.
The bigger gap: fairness in how output gets compared. Every technician stat in this report — jobs completed, hours logged, the Leaderboard — is a raw total, and a raw total flatters full-time staff over part-time ones by construction. Troy works roughly half the hours Jordan does; on every current metric that makes Troy look like he's produced half as much, when the honest comparison is output against the hours he was actually rostered for. As it stands, a part-time technician has to work twice as hard per rostered hour just to land in the same place on the board as a full-time one. The data to fix this already exists — rostered hours are tracked per person per week — the missing piece is a fairness-adjusted view (output per rostered hour, not just output) on the Analytics dashboard and the Leaderboard, so the comparison is against each person's actual availability, not everyone else's.
Short version: STRM 2.0 doesn't stop running without me, and it doesn't need me specifically to keep being extended. A couple of concrete things are still worth tidying up now, while they're an easy fix, rather than being discovered the hard way later.
STRM 2.0 runs on Cloudflare's infrastructure and a managed Neon database, deployed and live right now. It doesn't sit on my laptop or need me signed in to keep serving the shop's job board day to day.
It wasn't hand-built from tacit knowledge only I have — it was written by Claude Code, documented along the way (this report, plus the in-repo AGENTS.md/CLAUDE.md conventions file), with a full decision trail across 39 versioned releases of git history. Anyone with repo access and a Claude subscription could point Claude Code at it and pick up exactly where I left off — that's genuinely how this project itself started.
GitHub, Cloudflare, Neon, and the Shopify app credentials were all set up under Storm Rides' own support@stormrides.nz from day one of the project, not a personal account — the Cloudflare account itself confirms this directly. Not a single-point-of-failure the way it might look from the outside.
The harder-to-transfer part isn't the codebase — it's the four years of shop-floor context behind why things work the way they do. Some of that is now captured in this report and in the product decisions already shipped; the rest is worth writing down as it comes up, the same way AGENTS.md already captures the technical conventions.
What's actually left to do: keep AGENTS.md updated as a living document alongside future changes, so the next person — or the next AI-directed session — inherits the same context this one did. Beyond that, the honest answer is that this system was built by directing an AI coding tool that's available to anyone, not by writing code only one person can read — which makes continuity a smaller problem here than it would be for a traditionally hand-coded system maintained solo.
Everything below is a "what if," not a proposal in motion. STRM 2.0 was built for one shop. But nothing about the way it's built is Storm Rides-specific by necessity: the centralised access-control middleware, the single coherent data model, and the channel-based realtime layer are exactly the seams a second tenant would hook into, rather than a rewrite. This section names that possibility; it isn't a recommendation to act on it now.
Every page and API route already passes through one central permissions layer
(src/middleware.ts), not scattered checks — a tenant boundary slots into the same place.
The 25-model schema is already one coherent design, not years of organic sprawl. The realtime layer is
already channel-based ("roster", "label-print", "eftpos") —
extending a channel name to "roster:<shop-id>" is a small change, not a new
subsystem. None of this makes it a weekend job — it makes it a retrofit instead of a rebuild.
Every one of the 25 data models and 98 API routes needs a tenant boundary added and audited. Shopify integration moves from one hardcoded store token to a per-tenant OAuth connection. The label-printer and EFTPOS bridges move from secrets baked in at build time to a per-shop provisioning flow. Billing, self-serve signup, shop-specific settings (hours, tax rate, branding), and support/documentation for people who aren't already living in this codebase all need to exist.
| Workstream | Est. hours |
|---|---|
| Multi-tenant data isolation across all models & routes | 70–120 |
| Self-serve signup & subscription billing | 35–60 |
| Shopify OAuth (per-tenant store connections) | 25–45 |
| Hardware-bridge provisioning (label + EFTPOS, per shop) | 20–35 |
| Configurable branding, tax rate, shop hours per tenant | 15–25 |
| Docs, onboarding, basic support tooling | 15–25 |
| Security & data-isolation hardening for other businesses' customer data | 20–30 |
| Total, to a sellable multi-tenant MVP | ~200–340 hrs |
| Paying shops | At $99/mo | At $199/mo |
|---|---|---|
| 5 (a first pilot cohort) | $5,940/yr | $11,940/yr |
| 20 | $23,760/yr | $47,760/yr |
| 100 | $118,800/yr | $238,800/yr |
Intent: this section is an observation of what's technically possible, not a pitch. STRM 2.0 was built on Storm Rides' funding and data, so anything beyond internal use is entirely the business's call — this section exists only to name that the option is there.