The Origin
I worked shifts at PTO Ski & Snowboarding. Rental floors are chaos on a weekend — intake forms on paper, inventory tracked in someone's head, DIN settings calculated with a laminated chart taped to the workbench. When a tech calculates a binding release value wrong, the consequences aren't a 404. They're a torn ACL.
I kept thinking: this should be one piece of software. Not three apps and a spreadsheet. One.
That's how PowderLedger started.
The Technical Decisions
Why Next.js. The rental floor is a mix of back-office computers (Chrome on desktops) and technicians on tablets. I needed one codebase, responsive from phone-sized screens to 27" monitors, with instant page transitions because a tech on a busy Saturday doesn't have patience for a spinner. App Router, server components, and streaming gave me the right primitives.
Why Prisma + Neon. Rental shops are seasonal. Traffic goes from near-zero in summer to 500 check-ins a day in January. Neon's serverless Postgres autoscaled without me touching anything, and Prisma kept the schema typed from the database all the way to the React component. Every foreign key relationship is enforced both in Postgres and in TypeScript — bugs I would've shipped otherwise, caught at build time.
Why multi-tenant from day one. The first customer is PTO. The second will be someone else's shop. I didn't want to rebuild for multi-tenancy in v2, so organizations are first-class in the schema:
model Organization {
id String @id @default(cuid())
slug String @unique
name String
rentals Rental[]
equipment Equipment[]
users User[]
createdAt DateTime @default(now())
}Every other model has an organizationId. Every query is scoped by it. Role-based access on top of that. A week of discipline up front, zero pain later.
The Hardest Feature
ISO 11088 DIN Calculator. Binding release values — the numbers that tell the binding when to pop your boot loose in a crash — are governed by an ISO standard with multiple lookup tables, skier "type codes" (I, II, III), weight-band lookups, height-band adjustments, a boot-sole-length correction, and an age modifier. Miss any of it and you've shipped a safety liability.
I didn't trust an LLM to implement the standard from memory, and I didn't trust myself to transcribe the table without a typo. So the tables live as hand-curated typed constants, the calculator is pure functions with unit tests against worked examples from the ISO worksheet, and the UI disables "save" until every input is present. Three layers of defense for the one feature you can't afford to get wrong.
Live QR Check-In
The second hardest part was check-in. Customers arrive with a QR code from their online booking. The rental tech needs to scan it, pull the reservation, confirm the assigned equipment, and apply the calculated DIN — fast. Like, under-ten-seconds fast.
External scanner hardware is a non-starter for most shops. Every tablet has a camera. So I used @zxing/browser to decode QR codes from the live camera stream, directly in the browser, with no backend round-trip until the tech confirms. It works offline with a service worker caching the last active reservations. It works on a 2021 iPad that the shop already owns.
import { BrowserMultiFormatReader } from '@zxing/browser';
const reader = new BrowserMultiFormatReader();
const result = await reader.decodeOnceFromVideoDevice(undefined, videoEl);
// result.getText() is the reservation codeThat single library call replaced a $400 barcode scanner.
Going Live
The day we cut over, I was on-site. The anxiety of watching real staff press "check in" on software I wrote is different from anything you feel in a dev environment. It worked. A couple of small polish bugs (focus state on an iPad Mini, a too-aggressive session timeout), fixed in an hour, Vercel redeployed, back in action.
What I'd Do Differently
- Start with the service center module. I built intake first because it's the obvious front door. But the service center — where techs wax, tune, and prep equipment — turned out to be the part staff interact with most. I'd prioritize it earlier next time.
- Ship a tiny analytics view on day one. The owner asked on day one, "how many rentals today?" I built the dashboard in week three. Should've been week one.
- Offline-first, not offline-after. I bolted a service worker on later. With hindsight, assume the wifi dies mid-weekend and design for that from the schema up.
Read the full PowderLedger case study for architecture diagrams and feature screenshots.