Case Study
Drive Up
Online booking for a driving instructor in Edmonton: students book and prepay a lesson, manage it in their own portal, and the instructor runs the day from one console, in English and Ukrainian. In client review before launch.
- Period
- – present
- Role
- Sole engineer; in client review
8 days (13 to 21 September 2026)
First commit to staging
68
Commits, all mine
13
Architecture decision records
21
Test files
2 (English and Ukrainian)
Languages

Lesson booking for one driving instructor, from first commit to staging in eight days
Problem
One driving instructor teaches students in Edmonton, in English and Ukrainian. Students needed to book a lesson themselves and pay for it in advance, at a time that fits his working hours and leaves a buffer around a first-time driver's lesson, then move or cancel it within his rules. The decisions that are his had to stay his: rescheduling, a late cancellation, a no-show and how much is refunded.
What was built
Drive Up is one Next.js application with PostgreSQL in Docker. A visitor chooses English or Ukrainian on the first screen. A student proves a phone number with a code the app sends by SMS, behind a Cloudflare Turnstile check, then books in four steps: who they are, the lesson type, a day and time computed from the instructor's weekly schedule, blocked time, minimum notice, booking horizon and buffers, and payment. The price plus GST is paid in Stripe's embedded form, and only Stripe's signed payment event turns the 30-minute hold on the slot into a lesson.
In the student portal a student sees every lesson, can reschedule each one once, and can cancel with a full refund before the late-cancellation window. The instructor's console shows the day's agenda, the students with his private notes, and every action on a lesson: reschedule, cancel, mark a no-show and decide the penalty and the refund. Reminders and hold expiry run on a scheduler inside the server process. The instructor also manages the photos and videos behind the public pages and an intro of full-screen slides. Staging is deployed by GitLab CI to my VPS behind its shared Traefik; production has its own Docker Compose layer with Traefik and HTTPS, verified locally.
Outcome
Drive Up is in client review on staging, which sends no SMS and takes no real money; it goes to production when the client approves it. The first staging deploy checked the whole path: health over HTTPS, administrator sign-in, media uploads, and a booking paid with a Stripe test card that appeared on the instructor's day. The same deploy found two bugs, configuration keys missing from the container and cookie security tied to the wrong setting, and both were fixed that day. The source is private.
Shipping Timeline
From zero to production.
01 · Requirements
Baseline requirements
02 · Build
Night drive, in English and Ukrainian
The look was chosen from three live prototypes; the language is picked on the first visit.
03 · Build
Instructor sign-in, settings and media
04 · Architecture
Booking stage planned
Glossary, decisions and ADRs, and one issue per part.
05 · Payments
Booking with Stripe prepayment
Phone verification, the availability engine, holds and the embedded payment form.
06 · Build
Student portal
One reschedule per lesson, and refunds for cancellations.
07 · Build
The instructor's day
Agenda, student cards, lesson actions and penalties, reminders and SMS templates.
08 · Payments
Refunds and payment timing hardened
A refund Stripe failed is sent again; the time-dependent payment paths are tested.
09 · Deploy
Staging on my VPS
Deployed by GitLab CI behind the shared Traefik and checked end to end with a Stripe test booking.
10 · Support
Next: client approval, then production
Engineering
How it is built.
Stack
- TypeScript
- React
- Next.js
- Node.js
- PostgreSQL
- Docker
- GitLab CI/CD
- Traefik
- Encrypted secrets (SOPS, dotenvx)
- Vitest
- Stripe
Decisions
- Only a signed Stripe event creates a lesson
- Choosing a slot places a 30-minute hold; the lesson exists only after Stripe delivers a signed paid event. The handler locks the checkout and the availability together, so a repeated event never makes a second lesson. If a payment arrives after its hold expired and the slot is gone, the whole price and GST are refunded.
- Own verification codes over plain SMS
- Every code costs money, and a dedicated verification service charges a multiple of a plain message. The app generates, hashes, rate-limits and checks its own codes, sends them as ordinary messages, and puts a free Cloudflare Turnstile check in front of the request so bots cannot spend the SMS budget. Outside production no message is sent and a fixed code works.
- A fixed GST line instead of Stripe Tax
- The business sells only in Alberta, where the one sales tax is the federal GST. The rate lives in the instructor's settings and the app adds one tax line to each checkout, snapshotted on the lesson, so a later rate change never alters a paid lesson. Stripe Tax would compute the same number for a fee on every transaction.
- Reminders run inside the server process
- Production is one host with one application instance, and the project allows no services beyond PostgreSQL, the application and the proxy. A scheduler in the Next.js process polls the database and claims each item in a transaction; a reminder is recorded as sent before the message leaves, so a restart cannot send it twice.
- Encrypted configuration in the repository
- Each environment has one dotenvx-encrypted env file committed with the code, and CI holds one decryption key. Invalid configuration stops the process with a message naming every offending key, and no configured value is ever printed.




