← Portfolio

Ara: your NZ driving practice app

A React Native simulator for New Zealand's full-licence test. AI examiner Sam guides, questions, reroutes, and scores each drive.

Role  Solo DeveloperYear  2026
Goes deeperCase study available
Read →

Overview

NZ Drive Practice is a personal project: a mobile app that rehearses the New Zealand full-licence practical test end to end. An AI examiner named Sam directs the drive by voice, asks the driver to identify hazards and answer road-rule questions, and assesses speed, stops, and braking. The design goal is a fully hands-free session where the phone is never touched during the twenty-minute drive, and closing the gap to that goal is the project's central engineering challenge (see Known limitations).

The app is aimed in particular at drivers converting an overseas full licence to an NZ full licence, a path many new residents go through. It pairs live GPS routing with a voice-led examiner and a scoring model built around Road Code criteria.

Onboarding screen titled Pass your NZ practical test, listing live GPS routing, an AI voice examiner, and Road Code scoring
Onboarding: the app frames itself as a full simulation of the NZ full-licence test.

Beyond the product itself, this project is a deliberate case study in AI-assisted engineering with process. It is not vibecoding. Every structural decision is captured in an ADR, the build follows an MVP roadmap with explicit exit criteria, and the core logic is covered by deterministic replay tests.

The live drive

During a session the app runs a real, test-style route near the driver using live GPS. Sam speaks directions aloud and asks the driver to call hazards as they appear, mirroring the flow of an official assessment. A persistent session bar keeps the state visible and lets the driver end the session at any point.

Live GPS map during a practice drive, with an AI examiner voice prompt and an active-session bar
A live, dynamically re-routed drive with Sam speaking directions and an active-session control.

The route is not fixed. It is recalculated from the current GPS position whenever a leg is completed or the driver strays more than 300 metres off course, with a twenty-second debounce to avoid thrashing. Position itself is read from MapView.onUserLocationChange as the authoritative source, since followsUserLocation proved unreliable with the Google provider, with watchPositionAsync kept only as a fallback.

The debrief

After each session the app produces a feedback screen. It summarises the drive in plain language, notes what went well and what to improve, and breaks the performance down by category, hazard awareness, road rules knowledge, speed compliance, stop compliance, navigation, and session completion, each scored individually. The live examiner runs on a fast conversational model, while the final debrief is generated by a stronger one, so the summary reads like a real examiner talking the driver through their result. From there the app offers a clear next step: drive again or return home.

Session feedback screen with observations, areas to improve, and a per-category score breakdown
A structured debrief: narrative feedback plus a per-category score breakdown.

The scoring model is deliberately aligned to official NZTA categories, including critical errors and immediate-fail conditions, rather than an invented percentage (ADR-0005), so it can answer the question a learner actually asks: would I have passed. That NZTA-aligned engine is still being completed as MVP-1 work.

Session history

TODO: past-sessions screen (list of previous practice sessions shown before starting a new one)
TODO: caption for the past-sessions screen.

Before starting a new session, the driver sees their past practice sessions, so progress is visible over time and each drive builds on the last rather than standing alone.

Architecture

The most important structural decision is captured in ADR-0006: the exam logic was extracted into a pure engine (src/engine/) with no React, Expo, or globals. It receives plain data, GPS fixes, clock ticks, voice exchanges, and emits plain data, events, speech requests, and state transitions. useDrivingSession.ts then becomes a thin adapter that wires that engine to the device APIs: GPS, TTS, network, and checkpoints.

The trade-off is what makes this decision worth documenting. Previously the logic lived in a hook of around 350 lines, leaning on useRef to dodge stale closures and on module-level singletons. That was slow to test, since jest-expo renders hooks and the suite took roughly ten minutes, and it was fragile whenever state had to be reset. Separating the engine enables deterministic replay: a real GPS track plus a transcript can be replayed in a unit test in milliseconds. That is invaluable for testing exam logic without driving a car every time, and it is the piece that makes a later Android port (MVP-5) feasible by reusing the engine intact.

The AI layer is deliberately kept off the client. Both the live examiner and the debrief run behind an Edge Function proxy rather than being called directly from the device, which keeps credentials and prompt logic server-side. The backend is Supabase, with row-level security and Google OAuth.

Process

This project is structured to show engineering judgement, not just output. Each structural choice has an ADR that records the decision and the trade-off accepted with it. The build advances through MVPs with explicit exit criteria, so scope is bounded and progress is legible. And the deterministic replay tests mean the exam logic can be verified continuously, cheaply, and without a vehicle in the loop.

Key decisions and their trade-offs

| ADR | Decision | Trade-off accepted | | --- | --- | --- | | 0001 | All AI calls go through a Supabase Edge Function proxy | One extra network hop (tens of ms) in exchange for never shipping API keys in the client bundle, a blocker for distribution via TestFlight | | 0002 | Guest tier is fully ephemeral; nothing is persisted | Loses the option to migrate local progress into an account later, but avoids orphaned data and gives the simplest privacy story | | 0003 | Full-duplex vs half-duplex audio strategy, pending a spike | Replaced a voice library that crashed on an AVAudioPCMBuffer exception with expo-speech-recognition, but true always-listening is still unresolved; it is tap-to-speak today, which contradicts the core hands-free constraint | | 0004 | OSM / Overpass as the source of speed limits and checkpoints | Replaces a hardcoded 50 km/h limit and a sign-detection path that never fired, at the cost of an extra network call and imperfect data coverage in NZ | | 0005 | Scoring aligned to official NZTA categories (critical errors / immediate fail) rather than an invented percentage score | Answers the user's real question ("would I have passed?") but requires maintaining an event-to-category mapping table by hand | | 0006 | Pure, deterministic session engine | A large refactor cost, but it unlocks fast tests, replay, and Android portability |

Known limitations (active technical debt)

Being candid about what is not done yet is part of the point. The largest gap between the product vision and the current code is that true hands-free is not implemented; the session is tap-to-speak today, pending the ADR-0003 spike. Stop and intersection detection was originally based on parsing Google's directions text, which never worked reliably, and is being replaced by OSM (ADR-0004), but the NZTA-aligned scoring engine itself is still MVP-1 work and not yet complete. And simulate_drive.sh, referenced in CLAUDE.md, does not actually exist in the repo; its replacement, a route replayer built on Maestro, is scheduled for MVP-4.

Roadmap status

Per docs/ROADMAP.md (updated 2026-07-18, progress tracked through 21 July):

MVP-0, foundations, is complete: the AI proxy is in production, the schema is at v2 with checkpointing every sixty seconds, false reroutes are fixed, and CI is green with the test suite running in about four seconds, down from roughly ten minutes.

MVP-1, a credible exam, is in progress: the pure engine is extracted (ADR-0006), but real OSM data, classifying justified detours versus genuine errors, and NZTA scoring are all still outstanding.

MVP-2, true hands-free, is blocked on the ADR-0003 spike. MVP-3 (user tiers), MVP-4 (product quality), and MVP-5 (research and expansion) are not yet started.

Stack

Frontend is React Native with Expo, using a dev client rather than Expo Go because of the native voice module. Maps use react-native-maps over Google Maps (PROVIDER_GOOGLE). Voice combines expo-speech-recognition for speech-to-text with OpenAI TTS, falling back to expo-speech. The backend is Supabase (Postgres with RLS and Google OAuth). The AI examiner runs on Claude Haiku for the live conversation and Claude Sonnet for the final debrief, both behind an Edge Function proxy rather than called directly from the client.