Vehicle Manager Documentation

Welcome to the official documentation for Vehicle Manager - a comprehensive, self-hosted platform for tracking every aspect of vehicle ownership. Whether you manage a single car or a whole fleet of motorcycles, vans, and trucks, this guide will walk you through everything from initial setup to the deepest corners of the API.

Introduction

Vehicle Manager was built with a simple philosophy: you should own your data. There are no cloud subscriptions, no third-party analytics, and no telemetry. You host it yourself, and everything - every fuel receipt, every MOT advisory, every depreciation calculation - lives on your own infrastructure.

The platform covers fuel costs, service history, MOT records, insurance policies, road tax, parts, consumables, vehicle specifications, depreciation tracking, receipt OCR, report generation, and much more. It's built as three interlocking components:

⚙️

Backend API

Symfony 6.4 · PHP 8.4 · MySQL 8 · Redis 7

🖥️

Web Frontend

React 18 · MUI 5 · Charts · i18n (21 languages)

📱

Mobile App

React Native · TypeScript · Offline-first · 20 languages

Everything is open-source, free forever, and licensed permissively. The project lives on GitHub and contributions are always welcome.

Prerequisites

Before you begin, make sure your machine has the following installed. If you're deploying to a server, these same requirements apply there.

RequirementMinimum VersionNotes
Docker20.10+Docker Desktop or Docker Engine
Docker Compose1.29+ or v2Bundled with Docker Desktop; standalone works too
Git2.30+For cloning the repository
RAM4 GB freeMySQL, Redis, PHP-FPM, Nginx, and Node all run simultaneously
Disk2 GB freeDocker images plus room for uploads and database growth
💡 Local frontend development

If you plan to work on the React frontend outside of Docker, you'll also need Node.js 18+ and npm or yarn installed locally.

Docker Setup

The entire application runs inside Docker containers. Clone the repository, bring the containers up, and you're ready to go - no manual PHP, MySQL, or Redis installation required.

1. Clone the Repository

git clone https://github.com/peteclarke-del/vehicle.git
cd vehicle

2. Start the Containers

docker-compose up -d

This spins up five services. Here's what each one does and where it listens:

ServiceImagePortPurpose
mysqlmysql:8.03306Primary database - 512 MB memory limit, health-checked
redisredis:7-alpine6379Cache layer - 256 MB maxmemory, AOF persistence, noeviction policy
phpCustom PHP 8.4 FPM9000 (internal)Application server - Xdebug, Composer, 200 MB upload limit, 1 GB memory
nginxCustom Nginx8081Reverse proxy to PHP-FPM - serves the API
frontendCustom Node3000React dev server - hot-reload, 2 GB memory limit

3. Wait for Readiness

MySQL has a health check configured. The PHP entrypoint script waits for the database to be ready and then automatically runs Doctrine migrations. You can follow the logs to watch for completion:

docker-compose logs -f php

Once you see the FPM "ready to handle connections" message, you're good.

4. Environment Variables

The default .env file ships with sensible development defaults. For production, you'll want to configure these key variables:

VariablePurpose
DATABASE_URLMySQL connection string
REDIS_URLRedis connection string
JWT_SECRET_KEY / JWT_PUBLIC_KEY / JWT_PASSPHRASEJWT signing keys
APP_SECRETSymfony application secret
CORS_ALLOW_ORIGINAllowed CORS origins (production only)
DVLA_API_KEY / DVLA_CLIENT_ID / DVLA_CLIENT_SECRETUK DVLA vehicle lookup
DVSA_API_KEY / DVSA_CLIENT_ID / DVSA_CLIENT_SECRETUK DVSA MOT history
API_NINJAS_KEYVIN decoding and vehicle specifications
EBAY_CLIENT_ID / EBAY_CLIENT_SECRETeBay Browse API for part scraping

First Run

Once the containers are up and MySQL migrations have completed, you need to seed the reference data that powers the application's dropdowns and category systems.

Seed Lookup Data

docker-compose exec php bin/console doctrine:fixtures:load --no-interaction

This populates vehicle types (Motorcycle, Car, Van, Truck, EV), makes, models, consumable types, part categories, and security features from the bundled JSON data files in backend/data/.

Seed Feature Flags

docker-compose exec php bin/console app:seed-feature-flags

This creates the 49 default feature flags that control which parts of the application are available to each user. By default, all flags are enabled for everyone. Admins can override them per-user later.

Access the Application

InterfaceURL
Web Frontendhttp://localhost:3000
Backend APIhttp://localhost:8081/api
Health Checkhttp://localhost:8081/health

Creating an Account

Head to the web frontend at http://localhost:3000 and click Register. You'll need to provide your name, email address, and a password. The password must meet the security policy (minimum 8 characters with a mix of upper-case, lower-case, digits, and special characters).

Alternatively, you can register via the API directly:

curl -X POST http://localhost:8081/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "you@example.com",
    "password": "SecureP@ss1",
    "firstName": "Jane",
    "lastName": "Doe"
  }'

Becoming an Admin

The first user you create will have standard ROLE_USER permissions. To promote yourself to an administrator - which unlocks user management, feature flag control, and vehicle assignment features - run this SQL against your database:

UPDATE users SET roles = '["ROLE_USER","ROLE_ADMIN"]' WHERE id = 1;
⚠️ Admin privileges

Admins can see all vehicles across all users, manage feature flags, create and disable user accounts, force password changes, and assign vehicles between users. Grant this role carefully.

Architecture Overview

Vehicle Manager follows a clean API-first architecture. The backend exposes a RESTful JSON API that both the web and mobile front-ends consume identically. There is no server-side rendering - the backend is purely an API server, and the front-ends are fully decoupled single-page applications.

🔐

JWT Auth

Stateless tokens · 1-hour TTL · Refresh tokens · Header & query extraction

🗄️

Redis Cache

Tag-based invalidation · 5 named pools · 2 min–1 hr TTLs

📊

Doctrine ORM

27 entities · MySQL 8 · Migrations · Fixtures

🌍

Multi-platform

Web + Mobile · Shared API · Offline-capable

Authentication uses the Lexik JWT Authentication Bundle. Tokens have a one-hour time-to-live and can be extracted from both the Authorization: Bearer header and a ?token= query parameter (the latter is used for SSE notification streams). Refresh tokens are stored in the database with a 30-day expiry, and users can explicitly revoke them.

Caching runs through Redis 7 with tag-based invalidation. Five named cache pools cover different data types, each with a TTL tuned for how frequently the underlying data changes:

Cache PoolTTLUsed For
cache.vehicles10 minutesVehicle lists and detail responses
cache.dashboard2 minutesDashboard totals, cost breakdowns, stats
cache.preferences30 minutesUser preferences
cache.records5 minutesFuel, service, MOT, and other record lists
cache.lookups1 hourVehicle types, makes, models, categories

Backend Architecture

The backend is a Symfony 6.4 application running on PHP 8.4. It uses attribute-based routing on controller methods, Doctrine ORM for database access, and Symfony's autowiring container for dependency injection. The codebase is organised into these key layers:

  • Controllers (32 files) - Handle HTTP requests, validate input, enforce authorisation, and return JSON responses. Seven reusable traits provide common functionality like ownership verification, JSON validation, and entity hydration.
  • Services (16+ files) - Encapsulate business logic: cost calculation, depreciation, OCR, report generation, URL scraping (with adapters for eBay, Amazon, Shopify, and generic sites), VIN decoding, DVLA/DVSA integration, and import/export.
  • Entities (27 files) - Doctrine ORM entities mapping to MySQL tables. Rich relationships: User → Vehicles → all sub-records. Vehicle → Specification (OneToOne). InsurancePolicy ↔ Vehicles (ManyToMany).
  • Event Subscribers (3 files) - FeatureFlagSubscriber enforces per-user feature flags on API routes. TestAuthSubscriber enables mock authentication in test environments. PreventFixturesLoadSubscriber blocks fixture loading in production.
  • Commands (5 files) - Console commands for seeding feature flags, importing consumables/parts, migrating attachments, and normalising MOT advisories.
  • Data Fixtures (8 files) - Seed lookup data from bundled JSON files covering vehicle types, makes, models, consumable types, part categories, and security features.

Controller Traits

Seven traits keep controllers DRY:

TraitPurpose
AuthenticationRequiredTraitProvides getUserEntity() and admin-check helpers
JsonValidationTraitParses and validates JSON request bodies
OwnershipVerificationTraitVerifies the current user owns or is assigned to the vehicle/entity
EntityHydrationTraitPopulates entity fields from request data
DateSerializationTraitConsistent date formatting across responses
ReceiptAttachmentTraitLinks receipt attachments to records
UserSecurityTraitRole-based access checks (isAdminForUser())

Frontend Architecture

The web frontend is a React 18 single-page application bootstrapped with Create React App. It uses MUI 5 (Material UI) for the component library, React Router v6 for client-side routing, and Axios for HTTP communication with the backend.

The app is wrapped in a layered provider hierarchy: ThemeContext (MUI light/dark theming) → AuthContext (JWT token management, login/logout, user state) → UserPreferencesContext (default vehicle, distance unit, currency, rows per page) → VehicleContext (global vehicle list with 30-second caching) → PermissionsContext (feature-flag and assignment-based access control).

The main layout is an AppBar with a collapsible drawer. Navigation items in the drawer are drag-and-drop reorderable (via @dnd-kit) and their order is persisted to user preferences. The drawer can be pinned open or collapsed to a mini-rail (48 px icons only).

Charts are powered by @mui/x-charts - PieChart for cost breakdowns, BarChart for monthly spend, and LineChart for depreciation curves. Reports use 13 JSON template definitions and can be previewed in-browser via SheetJS (XLSX) or downloaded as PDF.

Mobile Architecture

The mobile app is built with React Native 0.73 and TypeScript. It uses React Native Paper (Material Design 3) for the UI layer and React Navigation for stack and tab-based navigation.

The defining feature of the mobile app is its dual-mode architecture. On first launch, the user is presented with a choice: connect to a remote Vehicle Manager server (web mode) or use the app entirely offline (standalone mode). In standalone mode, a LocalApiAdapter provides full CRUD operations backed by AsyncStorage, so the app works without any server at all.

In web mode, a SyncContext manages an offline change queue. When the device loses connectivity, mutations are queued in AsyncStorage and automatically replayed when the connection returns - with a 2-second debounce, up to 5 retries per operation, and automatic discard of 4xx errors (which indicate permanently invalid data). The useOfflineData hook provides cache-first loading with a 24-hour TTL, so the app always shows data even when offline.

Database Schema

The database is MySQL 8.0 with 27 Doctrine ORM entities. The central entity is Vehicle, which has cascade-remove relationships to almost everything else - deleting a vehicle automatically removes all of its fuel records, service records, parts, consumables, MOT records, road tax, todos, images, status history, and specification.

Key relationships to understand:

  • User → Vehicles - A user owns many vehicles (OneToMany)
  • Vehicle → Sub-records - Each vehicle has many fuel records, service records, parts, consumables, MOT records, road tax records, todos, images, and status history entries (all OneToMany with cascade remove)
  • Vehicle → Specification - Each vehicle has at most one specification entity (OneToOne with cascade remove), covering ~40 fields for engine, drivetrain, chassis, and dimensions
  • InsurancePolicy ↔ Vehicles - Policies can cover multiple vehicles, and a vehicle can be on multiple policies (ManyToMany via a join table)
  • ServiceRecord → ServiceItems - A service record contains multiple line items, each of which may link to a Part or Consumable
  • VehicleAssignment - Allows sharing vehicles between users with granular permissions (canView, canEdit, canAddRecords, canDelete)
  • FeatureFlag ↔ UserFeatureOverride ↔ User - 49 feature flags with per-user overrides

Authentication API

All authenticated endpoints require a Bearer token in the Authorization header. Tokens are issued on login, last one hour, and can be refreshed using a long-lived refresh token (30 days). The authentication flow is entirely stateless - there are no server-side sessions.

POST /api/login Authenticate and receive a JWT token

Submit your email and password to receive a JWT access token. This endpoint is handled by Symfony's JSON login firewall.

Request Body

FieldTypeRequiredDescription
emailstringYesYour registered email address
passwordstringYesYour password

Response (200)

{ "token": "eyJ0eXAiOiJKV1Qi..." }

Errors

401 - Invalid credentials.

POST /api/register Create a new user account

Register a new account. The user is automatically assigned ROLE_USER and default preferences are created. Country defaults to GB if not provided.

Request Body

FieldTypeRequired
emailstringYes
passwordstringYes
firstNamestringYes
lastNamestringYes
countrystring (2-letter code)No (defaults to GB)

Response (201)

{ "message": "User registered successfully", "userId": 1 }
GET /api/me Get current user profile, features, and assignments

Returns the authenticated user's profile information along with their resolved feature flags and any vehicle assignments they have.

Response (200)

{
  "id": 1,
  "email": "you@example.com",
  "firstName": "Jane",
  "lastName": "Doe",
  "roles": ["ROLE_USER"],
  "country": "GB",
  "isActive": true,
  "isVerified": false,
  "passwordChangeRequired": false,
  "features": { "dashboard": true, "fuel_records": true, ... },
  "vehicleAssignments": [ ... ]
}
PUT /api/profile Update your profile

Update your first name, last name, or email address.

Request Body

FieldType
firstNamestring
lastNamestring
emailstring
POST /api/auth/issue-refresh Issue a 30-day refresh token

Generates a long-lived refresh token (30 days) that can be used to obtain new JWT access tokens without re-authenticating. Requires IS_AUTHENTICATED_FULLY.

Response (200)

{ "refreshToken": "a1b2c3d4e5..." }
POST /api/auth/refresh Refresh your JWT using a refresh token

Exchange a valid refresh token for a new JWT access token. This is a public endpoint - no Bearer token required.

Request Body

{ "refreshToken": "a1b2c3d4e5..." }

Response (200)

{ "token": "eyJ0eXAiOiJKV1Qi..." }
POST /api/auth/revoke Revoke refresh token(s)

Revokes one or all refresh tokens for the authenticated user. Used during logout to invalidate sessions.

POST /api/change-password Change your password

Change your password. You must provide your current password for verification. The new password must meet the security policy.

Request Body

{ "currentPassword": "OldP@ss1", "newPassword": "NewP@ss2" }
POST /api/force-password-change/{id} Admin: force a user to change their password

Sets the passwordChangeRequired flag on the target user. On their next login, they will be forced to set a new password before they can do anything else. Requires ROLE_ADMIN.

Admin API

Every endpoint in this section requires ROLE_ADMIN. Administrators can manage user accounts, control feature flags on a per-user basis, and assign vehicles between users with granular permissions.

GET /api/admin/users List all users

Returns every registered user along with summary statistics (vehicle count, last login, active status).

POST /api/admin/users Create a new user

Create a user account on their behalf. Fields: email, password, firstName, lastName, and optionally roles.

GET /api/admin/users/{id} Get user details

Returns full details for a specific user, including their roles, active status, vehicle count, and last login time.

PATCH /api/admin/users/{id}/roles Update user roles

Replace a user's role array. Valid roles are ROLE_USER and ROLE_ADMIN.

Request Body

{ "roles": ["ROLE_USER", "ROLE_ADMIN"] }
PATCH /api/admin/users/{id}/toggle-active Enable or disable a user account

Toggles the user's isActive flag. Disabled users cannot authenticate.

PATCH /api/admin/users/{id}/force-password-change Force password change on next login

Sets passwordChangeRequired = true. The user will be prompted to set a new password on their next session.

GET /api/admin/feature-flags List all feature flags grouped by category

Returns all 49 feature flags organised by category (e.g., records, admin, vehicles, reports). Each flag includes its key, label, description, default state, and sort order.

GET /api/admin/users/{id}/features Get a user's feature overrides

Returns the effective feature flags for a specific user - the result of merging default flag values with any per-user overrides.

PUT /api/admin/users/{id}/features Bulk update feature overrides

Replace all feature overrides for a user. Send an object mapping feature keys to boolean values.

Request Body

{ "overrides": { "fuel_records": true, "mot_records": false, "reports": true } }
POST /api/admin/users/{id}/features/reset Reset feature overrides to defaults

Removes all per-user feature overrides, reverting the user to the system-wide default flag values.

GET /api/admin/users/{id}/assignments Get a user's vehicle assignments

Returns all vehicles assigned to this user (that they don't own) along with their permission levels.

PUT /api/admin/users/{id}/assignments Replace all vehicle assignments

Completely replaces the user's vehicle assignments. Each assignment specifies a vehicle and granular permissions.

Request Body

{ "assignments": [
  { "vehicleId": 5, "canView": true, "canEdit": true, "canAddRecords": true, "canDelete": false }
] }
DELETE /api/admin/users/{id}/assignments Remove all vehicle assignments

Revokes all vehicle assignments for this user. They will only be able to see their own vehicles.

POST /api/admin/seed-feature-flags Seed default feature flags

Creates or updates the 49 default feature flags in the database. Safe to run multiple times - existing flags are updated, not duplicated.

Vehicles API

The vehicle endpoints form the core of the application. Vehicles are the top-level entity that everything else hangs off - fuel records, services, parts, MOT history, and more. Admin users see all vehicles across all users; regular users see only their own vehicles plus any that have been assigned to them.

GET /api/vehicles List all vehicles

Returns all vehicles the authenticated user has access to. Results are cached for 10 minutes per user and automatically invalidated when vehicles are created, updated, or deleted. Admins see every vehicle in the system; regular users see vehicles they own plus any assigned to them.

Response

An array of vehicle objects, each including nested owner info, vehicle type, image URLs, computed fields (current mileage, MOT expiry, road tax expiry, insurance expiry, age, classic status), and relationship counts.

GET /api/vehicles/{id} Get a single vehicle

Returns full details for one vehicle including all computed fields, specification summary, and image list.

POST /api/vehicles Create a new vehicle

Create a new vehicle. The authenticated user is set as the owner. Vehicle type is required. Registration number and VIN are optional but recommended - they're used for DVLA/DVSA lookups and attachment file organisation.

Request Body

FieldTypeRequiredNotes
namestringYesDisplay name (e.g. "My Ford Focus")
vehicleTypeintYesVehicle type ID (1=Motorcycle, 2=Car, etc.)
makestringNoManufacturer
modelstringNoModel name
yearintNoYear of manufacture
registrationNumberstringNoUK or other registration plate
vinstringNo17-character VIN
purchaseCostdecimalNoPurchase price
purchaseDatedateNoYYYY-MM-DD
purchaseMileageintNoOdometer at purchase
vehicleColorstringNo
statusstringNoLive, Sold, Scrapped, or Exported (default: Live)
depreciationMethodstringNostraight_line, declining_balance, double_declining, or automotive_standard
depreciationYearsintNoDefault: 10
depreciationRatedecimalNoDefault: 20.00
motExemptboolNoOverride; auto-set for vehicles ≥30 years old
roadTaxExemptboolNoOverride; auto-set for vehicles ≥30 years old
securityFeaturesstringNoComma-separated or text description
PUT /api/vehicles/{id} Update a vehicle

Update any vehicle fields. If the status changes (e.g., from Live to Sold), a VehicleStatusHistory record is automatically created tracking the old status, new status, change date, and any notes.

DELETE /api/vehicles/{id} Delete a vehicle

Permanently deletes the vehicle and all associated records (fuel, service, parts, consumables, MOT, road tax, todos, images, specification, status history). This is a cascade delete and cannot be undone.

Response

204 No Content

GET /api/vehicles/{id}/depreciation Get depreciation schedule

Returns a year-by-year depreciation schedule for the vehicle. The schedule shows the value at the start of each year, the depreciation amount for that year, and the cumulative depreciation. Cached for 1 hour. Supports four methods:

  • automotive_standard (default) - 20% in year one, then 15% per year thereafter
  • straight_line - Equal depreciation each year (purchase cost ÷ years)
  • declining_balance - Fixed percentage of the remaining value each year
  • double_declining - Accelerated: double the straight-line rate applied to remaining value
GET /api/vehicles/{id}/costs Get cost breakdown

Returns a detailed breakdown of all costs associated with the vehicle, organised by category: fuel, parts, consumables, service, insurance, road tax, MOT, and depreciation. Cached for 2 minutes.

GET /api/vehicles/{id}/stats Get full vehicle statistics

Returns comprehensive statistics: total costs by category, cost per mile, average fuel consumption (MPG and L/100km), total mileage driven, and more. Cached for 2 minutes.

GET /api/vehicles/monthly-costs Monthly cost data for charts

Returns monthly fuel and maintenance costs per vehicle over a configurable period. This powers the dashboard bar charts. Also includes vehicleTotals - the total cost per vehicle across the period - used for the pie chart.

Query Parameters

ParamTypeDefaultDescription
monthsint6Number of months to look back (max 60)
GET /api/vehicles/totals Dashboard totals

Returns aggregated totals across all the user's vehicles for the dashboard stat cards: total fuel spend, total parts, total consumables, total service costs, total insurance, total road tax, total MOT costs, and overall total spend.

Query Parameters

ParamTypeDefaultDescription
periodint12Number of months to aggregate

Fuel Records API

Fuel records track every fill-up: date, litres, cost, mileage, fuel type, and station. The system automatically calculates MPG, price per litre, and cost per mile from the raw data. You can optionally attach a receipt photograph for OCR extraction.

GET /api/fuel-records/fuel-types List available fuel types

Returns a static list of fuel types: Petrol, Diesel, Premium Petrol, Premium Diesel, E10, E85, LPG, CNG, Hydrogen, Electric.

GET /api/fuel-records List fuel records

Returns fuel records, optionally filtered by vehicle.

Query Parameters

ParamTypeDescription
vehicleIdintFilter by vehicle (optional)
GET /api/fuel-records/{id} Get a single fuel record

Returns one fuel record with all computed fields (MPG, price per litre, cost per mile).

POST /api/fuel-records Create a fuel record

Request Body

FieldTypeRequired
vehicleIdintYes
datestring (YYYY-MM-DD)Yes
litresdecimalYes
costdecimalYes
mileageintYes
fuelTypestringNo
stationstringNo
notesstringNo
PUT /api/fuel-records/{id} Update a fuel record

Update any field on an existing fuel record.

DELETE /api/fuel-records/{id} Delete a fuel record

Permanently removes the fuel record. Returns 204.

ℹ️ Computed fuel metrics

Each fuel record automatically computes: calculateMpg() (miles per gallon from mileage difference and litres), getPricePerLitre(), calculateCostPerMile(), and convertMpgToLitres100km() for metric users.

Service Records API

Service records capture maintenance work - oil changes, brake replacements, tyre fitting, and anything else done to the vehicle. Each record can have multiple service items, each typed as a part, consumable, or labour charge. Items can be linked to existing Part or Consumable entities.

GET /api/service-records List service records

Query Parameters

ParamTypeDescription
vehicleIdintFilter by vehicle
unassociatedboolShow only records not linked to a vehicle
GET /api/service-records/{id} Get a service record with items

Returns the service record and its nested items array.

POST /api/service-records Create a service record

Request Body

FieldTypeRequired
vehicleIdintYes
serviceDatedateYes
serviceTypestringYes
laborCostdecimalNo
partsCostdecimalNo
mileageintNo
serviceProviderstringNo
workPerformedstringNo
nextServiceDatedateNo
nextServiceMileageintNo
notesstringNo
itemsarrayNo

Each item in the items array has: type (part/labour/consumable), description, cost, quantity, and optionally consumableId or partId to link to an existing entity.

PUT /api/service-records/{id} Update a service record

Updates the record and its items transactionally. Existing items can be updated or removed, and new items can be added in the same request.

DELETE /api/service-records/{id} Delete a service record

Deletes the service record and all its items. Returns 204.

GET /api/service-records/{id}/items Get service items

Returns just the items for a specific service record, without the parent record.

PATCH /api/service-items/{id} Update a single service item

Update one item's type, description, cost, quantity, or linked entity.

DELETE /api/service-items/{id} Delete a service item

Query Parameters

ParamTypeDescription
removeLinkedboolIf true, also deletes the linked Part or Consumable entity

Parts API

Parts represent physical components purchased for a vehicle - brake pads, filters, spark plugs, bulbs, and so on. Each part can be linked to a service record, MOT record, or todo. The URL scraper endpoint lets you pull product details directly from online retailers.

GET /api/parts List parts

Query Parameters

ParamTypeDescription
vehicleIdintFilter by vehicle
unassociatedboolShow only parts not linked to a vehicle
GET /api/parts/{id} Get a single part

Returns part details with linked records and receipt attachment info.

POST /api/parts Create a part

Request Body

FieldTypeRequired
vehicleIdintYes
name / descriptionstringYes
partNumberstringNo
manufacturerstringNo
supplierstringNo
costdecimalYes
quantityintNo (default 1)
purchaseDatedateYes
partCategoryintNo
warrantyMonthsintNo
installationDatedateNo
mileageAtInstallationintNo
productUrlstringNo
notesstringNo
includedInServiceCostboolNo
PUT /api/parts/{id} Update a part

Update any field on an existing part.

DELETE /api/parts/{id} Delete a part

Permanently removes the part record and any linked receipt attachment. Returns 204.

POST /api/parts/scrape-url Scrape product details from URL

Send a product URL and the system will attempt to extract the product name, price, description, images, and part number. Uses an adapter pipeline: eBay Browse API → Shopify → Amazon → eBay HTML → Generic DOM/OpenGraph/JSON-LD.

Request Body

{ "url": "https://www.ebay.co.uk/itm/123456789" }

Response

{
  "name": "NGK Spark Plug BKR6E-11",
  "price": 4.99,
  "description": "...",
  "imageUrl": "https://...",
  "partNumber": "BKR6E-11"
}

Consumables API

Consumables are items that need regular replacement - engine oil, coolant, brake fluid, tyres, chain lubricant, and so on. Unlike parts, consumables have replacement tracking: you can record the replacement interval (in miles), when it was last changed, and the mileage at change. The system uses this data to trigger notifications when a consumable is due for replacement.

GET /api/consumables List consumables

Query Parameters

ParamTypeDescription
vehicleIdintFilter by vehicle
unassociatedboolShow unlinked consumables
GET /api/consumables/{id} Get a single consumable

Returns consumable details with replacement tracking status.

POST /api/consumables Create a consumable

Key Fields

FieldTypeRequired
vehicleIdintYes
consumableTypeintYes
descriptionstringNo
brandstringNo
partNumberstringNo
costdecimalNo
quantitydecimalNo
supplierstringNo
replacementIntervalintNo
lastChangeddateNo
mileageAtChangeintNo
productUrlstringNo
includedInServiceCostboolNo
PUT /api/consumables/{id} Update a consumable

Update any consumable field, including replacement tracking data.

DELETE /api/consumables/{id} Delete a consumable

Returns 204.

POST /api/consumables/scrape-url Scrape product details from URL

Identical to the parts scraper - extracts product name, price, and description from a retailer URL.

MOT Records API

MOT records track the annual roadworthiness test required for vehicles in the UK. Each record captures the test result (Pass, Fail, or Advisory), costs, mileage, structured advisory and failure items, and repair details. The system integrates with the DVSA API to automatically import historical MOT data.

GET /api/mot-records List MOT records

Query Parameters

ParamType
vehicleIdint
GET /api/mot-records/{id} Get a single MOT record

Returns the full MOT record including parsed advisories and failures.

POST /api/mot-records Create a MOT record

Request Body

FieldTypeRequired
vehicleIdintYes
testDatedateYes
resultstringYes
testCostdecimalYes
repairCostdecimalNo
mileageintNo
testCenterstringNo
expiryDatedateNo
motTestNumberstringNo
testerNamestringNo
isRetestboolNo
advisoriestextNo
failurestextNo
repairDetailstextNo
notestextNo
PUT /api/mot-records/{id} Update a MOT record

Update any field on an existing MOT record.

DELETE /api/mot-records/{id} Delete a MOT record

Returns 204.

GET /api/mot-records/{id}/items Get related parts, consumables, and services

Returns all parts, consumables, and service records that are linked to this specific MOT record - useful for seeing what work was done to pass the test.

GET /api/mot-records/dvsa-history Fetch MOT history from DVSA

Queries the UK DVSA API to retrieve the full MOT test history for a vehicle registration. Returns test dates, results, mileage, advisories, and failures. Does not create records - use the import endpoint for that.

Query Parameters

ParamType
registrationstring (e.g. AB12CDE)
POST /api/mot-records/import-dvsa Import DVSA MOT history into records

Fetches MOT history from DVSA and creates MOT records for the specified vehicle. Skips any tests that have already been imported (matched by test number).

Request Body

{ "vehicleId": 1, "registration": "AB12CDE" }

Insurance API

Insurance is modelled with two levels. Insurance policies represent the actual policy documents from providers - these can cover multiple vehicles via a ManyToMany relationship. There are also vehicle-specific insurance records for simpler per-vehicle tracking. Both approaches are supported and you can use whichever fits your situation.

Policy Endpoints

GET /api/insurance/policies List all insurance policies

Returns all insurance policies for the authenticated user, with their attached vehicles.

POST /api/insurance/policies Create an insurance policy

Key Fields

FieldType
providerstring (required)
policyNumberstring
annualCostdecimal
startDate / expiryDatedate
coverageTypestring (Third Party, Third Party Fire & Theft, Comprehensive)
excessdecimal
ncdYearsint (no-claims discount years)
mileageLimitint
autoRenewalbool
notesstring
GET /api/insurance/policies/{id} Get a policy

Returns policy details with all attached vehicles.

PUT /api/insurance/policies/{id} Update a policy

Update any policy field.

DELETE /api/insurance/policies/{id} Delete a policy

Returns 204. Vehicle associations are removed but the vehicles themselves are not affected.

POST /api/insurance/policies/{id}/vehicles Attach a vehicle to a policy
{ "vehicleId": 5 }
DELETE /api/insurance/policies/{id}/vehicles/{vehicleId} Detach a vehicle from a policy

Removes the vehicle from this policy without deleting either entity.

Vehicle Insurance Endpoints

GET /api/insurance List vehicle insurance records

Filter by ?vehicleId=N for per-vehicle records.

POST /api/insurance Create vehicle insurance record

Simpler per-vehicle insurance entry for cases where a full policy model is overkill.

Road Tax API

Road tax records track vehicle excise duty (VED) payments. The system knows that vehicles aged 30 years or older are automatically road tax exempt in the UK, and will reject attempts to create road tax records for exempt vehicles. SORN (Statutory Off Road Notification) is also supported.

GET /api/road-tax List road tax records

Filter by ?vehicleId=N.

POST /api/road-tax Create a road tax record

Key Fields

FieldType
vehicleIdint (required)
startDatedate
expiryDatedate
amountdecimal
frequencystring (annual, 6_month - default: annual)
sornbool (default: false)
notesstring
⚠️ Tax-exempt vehicles

If the vehicle is 30+ years old (or has roadTaxExempt set to true), the API will return 400 and refuse to create the record.

PUT /api/road-tax/{id} Update a road tax record

Update dates, amount, frequency, SORN status, or notes.

DELETE /api/road-tax/{id} Delete a road tax record

Returns 204.

Todos API

Todos let you track upcoming work for a vehicle - "replace front tyre", "book MOT", "order new battery". Each todo can be linked to parts and consumables, and when you mark a todo as done, the completion timestamp is recorded. Overdue todos trigger notifications.

GET /api/todos List todos

Filter by ?vehicleId=N. Requires ROLE_USER.

POST /api/todos Create a todo

Request Body

FieldTypeRequired
vehicleIdintYes
titlestringYes
descriptionstringNo
dueDatedatetimeNo
PUT /api/todos/{id} Update a todo

Setting done: true automatically records the completedBy timestamp. Linked parts and consumables can also cascade their completion state.

DELETE /api/todos/{id} Delete a todo

Returns 204.

Attachments API

Attachments handle file uploads - primarily receipt photographs for OCR, but also general documentation, user manuals, and service manuals. Files are organised on disk into uploads/vehicles/{registration}/{category}/ for clean file management. The system validates MIME types and enforces a configurable size limit (default 200 MB).

When an admin user uploads an attachment for a vehicle they don't own, the attachment is automatically attributed to the vehicle's owner - not the admin.

POST /api/attachments Upload a file

Multipart form-data upload. Attach a file to a vehicle and optionally to a specific entity.

Form Fields

FieldTypeRequired
filefileYes
vehicleIdintNo
entityTypestringNo
entityIdintNo
categorystringNo
descriptionstringNo
GET /api/attachments/{id}/ocr OCR a receipt

Runs Tesseract OCR on the attachment image and returns structured data extracted from the receipt. The type parameter tells the engine what to look for.

Query Parameters

ParamValuesExtracted Fields
type=fuelfueldate, cost, litres, station, fuelType
type=partpartname, partNumber, price, quantity, supplier
type=consumableconsumablename, price, quantity, supplier
type=serviceservicedescription, laborCost, partsCost, date
GET /api/attachments List attachments

Filter by entityType, entityId, and/or category.

GET /api/attachments/{id} Download or get metadata

By default, streams the file for download. Add ?metadata=true to get JSON metadata instead (filename, MIME type, size, upload date).

PUT /api/attachments/{id} Update attachment metadata

Update the description, category, or entity association.

DELETE /api/attachments/{id} Delete an attachment

Deletes both the database record and the file from disk. Returns 204.

Reports API

The reporting system is template-driven. There are 13 built-in report templates covering fuel costs, service costs, part costs, consumable tracking, insurance and MOT expiry, road tax, and comprehensive vehicle reports. Reports can be downloaded as XLSX, PDF, or CSV.

GET /api/reports List saved reports

Returns previously generated reports for the authenticated user.

POST /api/reports Generate a report

Create a report from a template. Available template keys:

Template KeyDescription
fuel_costsFuel expenditure breakdown
service_costsService and maintenance costs
part_costsParts expenditure
consumables_totalTotal consumable spend
consumables_dueConsumables due for replacement
total_expenditureComplete cost summary across all categories
vehicle_reportFull single-vehicle report
expired_motVehicles with expired or expiring MOT
expired_insuranceExpired or expiring insurance policies
expired_road_taxExpired road tax
service_dueVehicles with upcoming service dates
insurance_dueInsurance renewals due
road_tax_dueRoad tax renewals due
GET /api/reports/{id}/download Download a generated report

Query Parameters

ParamValues
formatxlsx, pdf, csv

Notifications API

The notification system checks for items that need attention - MOT tests expiring soon, road tax renewals, overdue service dates, insurance expiry, consumables due for replacement, and todo deadlines. The web app uses Server-Sent Events (SSE) for real-time updates, with a fallback to polling.

GET /api/notifications Check for due notifications

Returns an array of notification objects, each with a type (mot, tax, service, insurance, consumable, todo), severity (info, warning, danger), vehicle info, and a human-readable message.

GET /api/notifications/stream SSE notification stream

Opens a Server-Sent Events stream that pushes notifications to the client in real time. The JWT token is passed as a query parameter since SSE doesn't support custom headers.

Query Parameters

ParamType
tokenstring (JWT token)

Lookups API

Lookup endpoints provide the reference data that populates dropdowns and selection lists throughout the application. All lookup data is cached for one hour. Vehicle types, makes, models, part categories, consumable types, and security features are seeded from the bundled JSON fixtures.

GET /api/vehicle-types List vehicle types

Returns: Motorcycle, Car, Van, Truck, EV. Cached 1 hour.

GET /api/vehicle-types/{id}/consumable-types Consumable types for a vehicle type

Returns consumable types relevant to the given vehicle type (e.g., motorcycles have chain lube; cars don't). Cached 1 hour.

GET /api/vehicle-makes List vehicle makes

Filter by ?vehicleTypeId=N. Cached 1 hour. You can also POST to create a custom make.

GET /api/vehicle-models List vehicle models

Filter by ?makeId=N and optionally &year=YYYY. Cached 1 hour. You can also POST to create a custom model.

GET /api/part-categories List part categories

Filter by ?vehicleTypeId=N. Cached 1 hour.

GET /api/security-features List security features

Filter by ?vehicleTypeId=N. Returns features like Disc Lock, Alarm, Tracker, Immobiliser. Cached 1 hour.

External API Integrations

Vehicle Manager integrates with several external APIs for UK vehicle data and product scraping. These require API keys configured via environment variables. All external calls include retry logic and graceful error handling.

DVLA - Vehicle Registration Lookup

GET /api/dvla/vehicle/{registration} Look up a UK vehicle registration

Queries the UK DVLA API and returns vehicle information: make, model, colour, fuel type, engine size, year of manufacture, tax status, and MOT status. Uses OAuth2 client-credentials flow with exponential backoff retry on 429 (rate limit) and 504 (timeout) responses. Results are cached.

DVSA - MOT History

GET /api/dvsa/vehicle/{registration} Get DVSA vehicle info

Returns vehicle details from the DVSA database.

GET /api/dvsa/mot-history/{registration} Get full MOT history

Returns every MOT test ever conducted on the vehicle - dates, results, mileage, advisories, and failure items.

GET /api/dvsa/latest-mot/{registration} Get latest MOT result only

Returns just the most recent MOT test result for quick status checks.

GET /api/dvsa/check Check DVSA API availability

Verifies that the DVSA API keys are configured and the service is reachable.

VIN Decoding

VIN decoding is available via GET /api/vehicles/{id}/vin-decode (documented under Vehicles API). It uses the API Ninjas service to decode 17-character VINs into make, model, year, country, and vehicle class.

Vehicle Specifications Scraping

Specification scraping is available via POST /api/vehicles/{id}/specifications/scrape (documented under Vehicles API). It uses an adapter pipeline with DVLA, API Ninjas Motorcycles, and API Ninjas Cars adapters.

Import & Export API

The import/export system lets you back up your entire vehicle collection and restore it elsewhere. Exports include all sub-records (fuel, service, parts, consumables, MOT, insurance, road tax, todos, specifications, images). ZIP exports also include the actual attachment files.

GET /api/vehicles/export Export vehicles

Query Parameters

ParamValuesDefault
formatjson, csv, xlsxjson

Returns all vehicles the user owns, with all nested records. Batch-processed (25 vehicles per batch) with a 1 GB memory limit to handle large collections.

GET /api/vehicles/export-zip Export as ZIP with attachments

Same as above, but the response is a ZIP file containing the JSON export plus all attachment files from the uploads/ directory.

POST /api/vehicles/import Import vehicles from JSON or CSV

Upload a previously exported file. The import is fully transactional - either everything succeeds, or nothing is committed. All sub-entities are created, including fuel records, service records, parts, consumables, MOT records, insurance, road tax, todos, and specifications. Duplicate VINs and registration numbers are handled gracefully.

POST /api/vehicles/import-zip Import from ZIP with attachments

Imports the JSON data and restores attachment files to the correct directories.

DELETE /api/vehicles/purge-all Delete all vehicles

Permanently deletes all vehicles belonging to the authenticated user. This is a destructive, irreversible operation.

Query Parameters

ParamTypeDescription
cascadeboolAlso delete all sub-records (default: true)

System API

A handful of utility endpoints for health checking, diagnostics, and third-party integrations.

GET /health Health check

Returns "OK" with a 200 status. No authentication required. Use this for Docker health checks or load balancer probes.

GET /api/system-check System diagnostics

Checks database connectivity and verifiable writable paths (uploads, var/cache, var/log). No authentication required.

POST /api/client-logs Client-side log receiver

Accepts log messages from the frontend for server-side aggregation. No authentication required. Useful for debugging client-side issues in production.

GET /api/ebay/webhook/account-deletion eBay marketplace compliance webhook

Handles eBay marketplace account deletion challenge and notification webhooks. Required for eBay API compliance.

Cost Calculator Service

The CostCalculator is the engine behind every cost display in the application. It queries across all record types - fuel, parts, consumables, service, insurance, road tax, and MOT - to compute total costs, per-category breakdowns, cost per mile, average fuel consumption (in both MPG and L/100km), and comprehensive vehicle statistics. It depends on the Entity Manager for database queries and the Depreciation Calculator for current-value computations.

The dashboard totals endpoint, individual vehicle stats, and cost breakdown pages all delegate to this service. Results are cached at the controller level (2-minute TTL in the dashboard pool) to avoid expensive re-computation on every page load.

Depreciation Calculator Service

The DepreciationCalculator implements four depreciation methods and generates year-by-year schedules showing the vehicle's value over time:

  • automotive_standard (default) - Mimics real-world vehicle depreciation: 20% in the first year, then 15% per year thereafter. This is the most realistic for most vehicles.
  • straight_line - Equal depreciation each year: purchase cost divided by the number of years.
  • declining_balance - A fixed percentage of the remaining value each year (the rate is configurable, default 20%).
  • double_declining - An accelerated method: double the straight-line rate applied to the remaining value each year.

The schedule output includes the value at the start of each year, the depreciation amount, and the cumulative depreciation. This powers the depreciation LineChart on the vehicle details page.

DVLA API Service

The DvlaApiService integrates with the UK Driver and Vehicle Licensing Agency API for vehicle registration lookups. It supports two authentication modes: OAuth2 client-credentials flow (using client ID and secret) and direct API key authentication.

The service implements exponential backoff retry logic for 429 (rate limited) and 504 (gateway timeout) responses. If all retries are exhausted, it throws a DvlaBusyException. Successful responses are cached to avoid repeated API calls for the same registration.

DVSA API Service

The DvsaApiService connects to the UK Driver and Vehicle Standards Agency API for MOT history retrieval. Like the DVLA service, it supports both OAuth2 and API key authentication.

Key methods:

  • getMotHistory(registration) - Full MOT test history
  • parseFailureItems(data) - Extracts structured failure items from raw data
  • parseAdvisoryItems(data) - Extracts structured advisory items
  • getCurrentMotStatus(data) - Derives the current MOT status
  • getPassRate(data) - Calculates the vehicle's overall MOT pass rate

Feature Flag Service

The FeatureFlagService manages the 49 feature flags that control which parts of the application are available to each user. The service resolves effective flags by merging the system-wide defaults with any per-user overrides. Admin users automatically get all features enabled, regardless of overrides.

Key methods:

  • getEffectiveFlags(user) - Returns the resolved flag map for a user
  • isFeatureEnabled(user, featureKey) - Check a single flag
  • setFeatureOverride(user, featureKey, enabled) - Set one override
  • bulkSetFeatureOverrides(user, overrides) - Set many overrides at once
  • resetFeatureOverrides(user) - Remove all overrides, revert to defaults
  • seedDefaults() - Create or update the 49 default flags
  • getAllFlagsGrouped() - Returns flags organised by category for the admin UI

The FeatureFlagSubscriber event subscriber enforces these flags on every API request. If a user tries to access a route that requires a disabled feature, they receive a 403 response. Routes for authentication, admin, and lookups are always allowed regardless of flags.

Receipt OCR Service

The ReceiptOcrService uses Tesseract OCR to extract structured data from photographs of receipts. When you upload a receipt and trigger OCR, the service analyses the image and returns field values that can pre-fill forms automatically.

Supported receipt types:

  • Fuel receipts - Extracts date, total cost, litres, station name, and fuel type
  • Part receipts - Extracts product name, part number, price, quantity, and supplier
  • Consumable receipts - Same as part receipts
  • Service receipts - Extracts work description, labour cost, parts cost, and date
💡 OCR accuracy tips

For best results, photograph receipts in good lighting against a dark background. Avoid wrinkles, folds, and reflections. Thermal receipts (common at petrol stations) should be photographed promptly as they fade over time.

Report Engine

The ReportEngine is a template-driven report generation system. Each report is defined by a JSON template that specifies data sources, calculations, layout, and styling. The engine reads these templates, queries the database for the relevant data, performs calculations, and outputs the result as XLSX (via PhpSpreadsheet) or PDF (via TCPDF).

The system supports unit conversion - distances can be rendered in miles or kilometres based on user preference. There are 13 built-in templates (listed in the Reports API section above), and the template format is extensible for custom reports.

URL Scraper Service

The UrlScraperService extracts product information from retail URLs. When you paste a product URL into the parts or consumables form, this service attempts to pull the product name, price, description, images, and part numbers automatically.

It uses an adapter pipeline - each adapter is tried in order until one succeeds:

  1. eBay Browse API - Fast path for eBay links using the official API (client credentials flow)
  2. Shopify Adapter - Parses Shopify product JSON and HTML
  3. Amazon Adapter - Uses the Amazon product API (API key required)
  4. eBay HTML Adapter - Fallback HTML parsing for eBay when the API isn't available
  5. Generic DOM Adapter - Last resort: parses OpenGraph meta tags, JSON-LD, and DOM elements

The service handles bot detection, retries on transient failures, and gracefully returns partial results when not all fields can be extracted.

Vehicle Export Service

The VehicleExportService serialises vehicles and all their sub-records into JSON format. It processes vehicles in batches of 25 with a 1 GB memory limit to handle large collections. The ZIP mode additionally packages all attachment files into the archive, preserving the directory structure.

Vehicle Import Service

The VehicleImportService is approximately 2,800 lines of import logic that handles every entity type. Imports are fully transactional - if any part of the import fails, the entire operation is rolled back. The service creates vehicles and all sub-entities (fuel records, service records with items, parts, consumables, MOT records, insurance policies, road tax, todos, attachments, and specifications). It handles duplicate detection, ID remapping, and relationship reconstruction.

Specification Scraper Service

The VehicleSpecificationScraperService fetches vehicle specifications from external sources using an adapter pattern. Three adapters are registered:

  • DvlaAdapter - Extracts specification data from DVLA vehicle records
  • ApiNinjasMotorcycleAdapter - Queries the API Ninjas Motorcycles API for detailed specs
  • ApiNinjasCarAdapter - Queries the API Ninjas Cars API

Adapters are tried in priority order. Results are merged to fill gaps - if one adapter returns engine specs but not chassis data, the next adapter can fill in the missing fields. Covers approximately 40 specification fields including engine, fluids, drivetrain, chassis, brakes, tyres, dimensions, weights, and performance data.

VIN Decoder Service

The VinDecoderService decodes Vehicle Identification Numbers via the API Ninjas VIN API. It validates the 17-character format (rejecting VINs containing I, O, or Q, which are never used in valid VINs), sends the query, and returns decoded information: make, model, year, country of manufacture, and vehicle class. Decoded data is cached on the vehicle entity to avoid repeated API calls.

Attachment Linking Service

The AttachmentLinkingService manages bidirectional links between attachments and entities. When you link a receipt to a fuel record, this service updates both the attachment's entity reference and the fuel record's receipt reference. It also handles file reorganisation - when an attachment is linked to a vehicle, the physical file is moved from the generic uploads directory to uploads/vehicles/{registration}/{category}/ for clean file management.

Entity Reference

This section documents every Doctrine entity in the system - the database tables, their fields, types, and relationships. Click on an entity card to expand its field list.

User

User users
FieldTypeNotes
idint (PK, auto)
emailstring(180), uniqueLogin identifier
rolesjsonDefault: ["ROLE_USER"]
passwordstring, nullableHashed (bcrypt/argon2)
firstNamestring(100)
lastNamestring(100)
countrystring(2)Default: 'GB'
passwordChangeRequiredboolDefault: false
isActiveboolDefault: true
isVerifiedboolDefault: false
createdAtdatetime
updatedAtdatetime, nullable
lastLoginAtdatetime, nullable

Relationships: OneToMany → Vehicle (owner)

Vehicle

Vehicle vehicles
FieldTypeNotes
idint (PK)
ownerManyToOne → UserRequired
vehicleTypeManyToOne → VehicleTypeRequired
namestring(100)Display name
makestring(50), nullable
modelstring(50), nullable
yearint, nullable
vinstring(17), unique, nullable
vinDecodedDatajson, nullableCached VIN decode result
registrationNumberstring(20), nullable
engineNumberstring(50), nullable
v5DocumentNumberstring(50), nullableUK V5C reference
purchaseCostdecimal(10,2)
purchaseDatedate
purchaseMileageint, nullable
vehicleColorstring(20), nullable
statusstring(20)Live, Sold, Scrapped, Exported
depreciationMethodstring(20)Default: automotive_standard
depreciationYearsintDefault: 10
depreciationRatedecimal(5,2)Default: 20.00
serviceIntervalMonthsintDefault: 12
serviceIntervalMilesintDefault: 4000
roadTaxExemptbool, nullableOverride; auto at age ≥ 30
motExemptbool, nullableOverride; auto at age ≥ 30
securityFeaturestext, nullable
createdAt / updatedAtdatetime

Computed fields: currentMileage (max fuel record mileage), lastServiceDate, motExpiryDate, roadTaxExpiryDate, insuranceExpiryDate, age, isClassic (≥25 yr), isRoadTaxExempt (≥30 yr), isMotExempt (≥30 yr).

Relationships: OneToMany → FuelRecord, ServiceRecord, Part, Consumable, MotRecord, RoadTax, Todo, VehicleImage, VehicleStatusHistory, InsurancePolicies (ManyToMany). OneToOne → Specification. All cascade remove.

Fuel Record

FuelRecord fuel_records
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
datedate
litresdecimal(8,2)
costdecimal(8,2)
mileageint
fuelTypestring(50), nullable
stationstring(200), nullable
notestext, nullable
receiptAttachmentManyToOne → Attachment (SET NULL)
createdAtdatetime

Computed methods: calculateMpg(), getPricePerLitre(), calculateCostPerMile(), convertMpgToLitres100km()

Service Record & Service Item

ServiceRecord service_records
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
serviceDatedate
serviceTypestring(50)
laborCostdecimal(10,2)
partsCostdecimal(10,2)
consumablesCostdecimal(10,2), nullable
additionalCostsdecimal(10,2)
mileageint, nullable
serviceProviderstring(100), nullable
workPerformedtext, nullable
nextServiceDate / nextServiceMileagedate / int
motRecordManyToOne → MotRecord
includedInMotCostbool (default true)
includesMotTestCostbool (default false)
notes, createdAt, receiptAttachment-

Relationships: OneToMany → ServiceItem (cascade persist/remove, orphanRemoval)

ServiceItem service_items
FieldType
idint (PK)
serviceRecordManyToOne → ServiceRecord (CASCADE)
typestring(20) - part, labour, consumable
descriptionstring(255), nullable
costdecimal(10,2)
quantitydecimal(10,2), default 1.00
consumableManyToOne → Consumable (SET NULL)
partManyToOne → Part (SET NULL)

Part

Part parts
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
name / descriptionstring(200)
partNumberstring(100), nullable
skustring(100), nullable
manufacturerstring(100), nullable
supplierstring(100), nullable
costdecimal(10,2)
pricedecimal(10,2), nullable
quantityint, default 1
purchaseDatedate
partCategoryManyToOne → PartCategory (SET NULL)
warrantyMonthsint, nullable
installationDatedate, nullable
mileageAtInstallationint, nullable
productUrlstring(500), nullable
imageUrlstring(500), nullable
includedInServiceCostbool, default false
serviceRecordManyToOne → ServiceRecord (SET NULL)
todoManyToOne → Todo (SET NULL)
motRecordManyToOne → MotRecord (SET NULL)
receiptAttachmentManyToOne → Attachment (SET NULL)
notes, createdAt-

Consumable

Consumable consumables
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
consumableTypeManyToOne → ConsumableType
descriptionstring(200), nullable
brandstring(100), nullable
partNumberstring(100), nullable
costdecimal(10,2), nullable
quantitydecimal(8,2), nullable
supplierstring(100), nullable
replacementIntervalint (miles), nullable
nextReplacementint (miles), nullable
lastChangeddate, nullable
mileageAtChangeint, nullable
productUrlstring(500), nullable
includedInServiceCostbool, default false
serviceRecord, todo, motRecord, receiptAttachmentManyToOne (SET NULL)
notes, createdAt, updatedAt-

MOT Record

MotRecord mot_records
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
testDatedate
resultstring(20) - Pass, Fail, Advisory
testCostdecimal(10,2)
repairCostdecimal(10,2), default 0
mileageint, nullable
testCenterstring(100), nullable
expiryDatedate, nullable
motTestNumberstring(50), nullable
testerNamestring(100), nullable
isRetestbool, default false
advisories, failures, repairDetails, notestext, nullable
receiptAttachmentManyToOne → Attachment (SET NULL)
createdAtdatetime

Insurance Policy

InsurancePolicy insurance_policies
FieldType
idint (PK)
providerstring(100)
policyNumberstring(100), nullable
annualCostdecimal(10,2), nullable
startDate / expiryDatedate, nullable
coverageTypestring(50), nullable
excessdecimal(10,2), nullable
ncdYearsint, nullable
mileageLimitint, nullable
autoRenewalbool, default false
notestext, nullable
createdAtdatetime

Relationships: ManyToMany → Vehicle (join table: insurance_policy_vehicles)

Road Tax

RoadTax road_tax
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
startDate / expiryDatedate, nullable
amountdecimal(10,2), nullable
frequencystring(10), default 'annual'
sornbool, default false
notestext, nullable
createdAtdatetime

Todo

Todo todos
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
titlestring(255), NotBlank
descriptiontext, nullable
donebool, default false
dueDatedatetime, nullable
completedBydatetime, nullable
createdAt / updatedAtdatetime

Relationships: OneToMany → Part, OneToMany → Consumable

Attachment

Attachment attachments
FieldType
idint (PK)
filenamestring(255)
originalNamestring(255)
vehicleManyToOne → Vehicle (CASCADE), nullable
userManyToOne → User (CASCADE)
mimeTypestring(100)
fileSizeint
entityTypestring(50), nullable
entityIdint, nullable
categorystring(50), nullable
descriptiontext, nullable
storagePathstring(255), nullable
uploadedAtdatetime

Specification

Specification specifications

OneToOne with Vehicle (CASCADE). Approximately 40 string fields covering:

  • Engine: type, displacement, power, torque, compression, bore, stroke, fuelSystem, cooling, sparkplugType
  • Fluids: coolantType/Capacity, engineOilType/Capacity, transmissionOilType/Capacity, middleDriveOilType/Capacity
  • Drivetrain: gearbox, transmission, finalDrive, clutch
  • Chassis: frame, frontSuspension, rearSuspension, staticSagFront/Rear
  • Brakes: frontBrakes, rearBrakes
  • Tyres: frontTyre, rearTyre, frontTyrePressure, rearTyrePressure
  • Dimensions: frontWheelTravel, rearWheelTravel, wheelbase, seatHeight, groundClearance
  • Weights: dryWeight, wetWeight
  • Performance: fuelCapacity, topSpeed
  • Metadata: additionalInfo, scrapedAt, sourceUrl

Vehicle Image

VehicleImage vehicle_images
FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
pathstring(255)
captionstring(255), nullable
isPrimarybool, default false
displayOrderint, default 0
isScrapedbool, default false
sourceUrlstring(255), nullable
uploadedAtdatetime

Vehicle Assignment

VehicleAssignment vehicle_assignments

Unique constraint on (vehicle_id, assigned_to_id).

FieldType
idint (PK)
vehicleManyToOne → Vehicle (CASCADE)
assignedToManyToOne → User (CASCADE)
assignedByManyToOne → User (SET NULL), nullable
canViewbool, default true
canEditbool, default true
canAddRecordsbool, default true
canDeletebool, default false
createdAt / updatedAtdatetime

Lookup Entities

These are simple reference entities that power dropdowns and categorisation throughout the application. They are seeded from bundled JSON fixtures.

EntityTableKey Fields
VehicleTypevehicle_typesid, name (Motorcycle, Car, Van, Truck, EV)
VehicleMakevehicle_makesid, name, vehicleType, isActive
VehicleModelvehicle_modelsid, name, make, vehicleType, startYear, endYear, imageUrl, isActive
ConsumableTypeconsumable_typesid, name, unit, description, vehicleType
PartCategorypart_categoriesid, name, description, vehicleType
SecurityFeaturesecurity_featuresid, name, description, vehicleType

Feature Flag Entities

EntityTableKey Fields
FeatureFlagfeature_flagsid, featureKey (unique), label, description, category, defaultEnabled, sortOrder, createdAt
UserFeatureOverrideuser_feature_overridesid, user, featureFlag, enabled, setBy, createdAt, updatedAt. Unique on (user_id, feature_flag_id)
UserPreferenceuser_preferencesid, user, name, value, createdAt, updatedAt
RefreshTokenrefresh_tokensid, refreshToken (unique), user, expiresAt, createdAt
Reportreportsid, user, name, templateKey, payload (json), vehicleId, generatedAt
VehicleStatusHistoryvehicle_status_historiesid, vehicle, user, oldStatus, newStatus, changeDate, notes, createdAt

Frontend Overview

The web frontend is a React 18 single-page application built with Create React App. It uses MUI 5 for the component library (including @mui/x-charts for data visualisation), React Router v6 for client-side routing, Axios for HTTP, and react-i18next for internationalisation with 21 languages (including a Pirate Easter egg 🏴‍☠️).

The application wraps all pages in a layered context hierarchy: ThemeContext → AuthContext → UserPreferencesContext → VehicleContext → PermissionsContext. This means every page has access to the current theme, authentication state, user preferences, the global vehicle list, and feature-flag permissions - without prop drilling.

Key Pages

PageRouteDescription
Dashboard/Stat cards, pie chart (cost per vehicle), bar charts (monthly fuel & maintenance), vehicle cards with status tabs
Vehicles/vehiclesVehicle list with card/table toggle, status filter, sort, pagination, add/edit dialog
Vehicle Details/vehicles/:id7 tabs: Overview, Statistics, Depreciation, Specifications, Pictures, Documentation, Manuals
Fuel Records/fuelFuel record list with vehicle selector
Service Records/serviceService records with item details and cost totals
Parts/partsParts list with linked MOT/service popups
Consumables/consumablesConsumables with replacement tracking
MOT Records/motMOT records with DVSA import
Insurance/insuranceInsurance policies with expiry highlighting
Road Tax/road-taxRoad tax records with SORN tracking
Todos/todoTask list with linked parts/consumables
Reports/reports13 templates, XLSX preview, PDF/XLSX download
Import/Export/tools/import-exportJSON/ZIP import with preview, filtered export
Profile/profileEdit profile, change password
Admin Users/admin/usersUser management (admin only)
Admin User Details/admin/users/:idFeature flags and vehicle assignments per user

Dashboard

The dashboard is the landing page after login. It shows stat cards at the top summarising total spend across categories (fuel, parts, consumables, services) for a configurable period. Below that is a pie chart showing total cost per vehicle (vehicles beyond the top 9 are grouped into an "Other" slice), and two stacked bar charts showing monthly fuel and maintenance spend over 3/6/12/24/36 months. At the bottom, vehicle cards are displayed in status tabs (Live, Sold, Scrapped, Exported) with key metrics and a drag-to-reorder capability.

Vehicles & Vehicle Details

The Vehicles page supports both card and table view modes with a toggle. Vehicles can be filtered by status, sorted by various fields (with sort preference persisted), and paginated. The add/edit dialog supports registration lookup (which scrapes data from the DVLA), make/model/year selection, depreciation settings, and MOT/road-tax exempt toggles.

The Vehicle Details page has seven tabs. Overview shows basic info, VIN decoder, and license plate rendering. Statistics displays cost breakdowns in a pie chart. Depreciation shows a line chart with the year-by-year schedule. Specifications shows ~40 fields with a "Scrape from web" button. Pictures is an image gallery with upload, reorder, and primary selection. Documentation and Manuals handle PDF/document uploads per category.

Records & Forms

All record pages (Fuel, Service, Parts, Consumables, MOT, Insurance, Road Tax, Todos) follow a consistent pattern: a list view with a vehicle selector filter, action buttons for add/edit/delete, and a dialog-based form. Each form handles receipt photograph upload, URL scraping (for parts and consumables), and entity linking.

Service record forms are the most complex - they support inline addition of service items, each typed as a part, labour charge, or consumable, with the ability to link to existing Part or Consumable entities. MOT record forms include structured advisory and failure item entry. Insurance forms support multi-vehicle policy assignment.

Admin Panel

The admin panel is available to users with ROLE_ADMIN. The Admin Users page lists all registered users with their vehicle counts, last login times, and active status. Admins can create new users, toggle active status, force password changes, and manage roles.

The Admin User Details page has two sections: feature flags (categorised toggle switches for all 49 flags with bulk update and reset-to-defaults) and vehicle assignments (assign vehicles to the user with granular canView/canEdit/canAddRecords/canDelete permissions).

Reports

The Reports page lets you generate reports from 13 built-in templates. Select a template, optionally filter by vehicle and date range, and generate. The system shows an in-browser XLSX preview powered by SheetJS, so you can inspect the data before downloading. Downloads are available in XLSX, PDF, and CSV formats.

Contexts & Hooks

The frontend uses five React contexts:

  • AuthContext - Manages JWT tokens (SafeStorage), login/logout, user profile, 401 interceptor
  • ThemeContext - MUI light/dark theme, persisted to localStorage and user preferences
  • UserPreferencesContext - Default vehicle, rows per page, distance unit, currency
  • VehicleContext - Global vehicle list with 30-second cache, auto-fetch on mount
  • PermissionsContext - Feature-flag and vehicle-assignment based access control; admin bypass

And seven custom hooks:

  • useApi - Generic data fetching with loading/error state
  • useDistance - Distance unit conversion (miles/km) based on user preference
  • useFileDrop - Drag-and-drop file handling
  • useNotifications - SSE stream with fallback polling, 90-day dismissed/snoozed cleanup
  • useSortPreference - Persisted sort field/order
  • usePagination - Pagination state with user-preferred default rows
  • useVehicleSelection - Vehicle selection synced with user preferences

Internationalization

The web app supports 21 languages via react-i18next, loading translations from /public/locales/{lang}/translation.json. The languages are: Arabic, Czech, Danish, German, English, Spanish, Finnish, French, Hindi, Italian, Japanese, Korean, Dutch, Norwegian, Pirate 🏴‍☠️, Polish, Portuguese, Russian, Swedish, Turkish, and Chinese.

The language selector is in the user preferences dialog and persists the choice to both localStorage and the server. Currency is auto-mapped from the language/locale choice.

Mobile App Overview

The mobile app is built with React Native 0.73 and TypeScript. It uses React Native Paper (Material Design 3) for the UI, React Navigation for navigation, and Axios for HTTP communication. The app supports 20 languages with bundled JSON translations (no Pirate - that's a web-only Easter egg).

The bottom tab navigation has five tabs: Dashboard, Vehicles, Fuel, Service, and More. The "More" tab provides access to MOT Records, Parts, Consumables, Vehicle Lookup, and Settings. All screens are feature-gated through the PermissionsContext.

Web & Standalone Modes

The defining feature of the mobile app is its dual-mode architecture. On first launch, the ServerConfigScreen presents two choices:

  • Connect to Server - Enter your Vehicle Manager server URL. The app tests the connection before proceeding. All data is synced with the backend. The URL is auto-normalised (adds https:// if missing, appends /api if needed).
  • Use Standalone - The app runs entirely offline. A LocalApiAdapter provides full CRUD operations backed by AsyncStorage. No server required. Data lives entirely on the device.

In standalone mode, the PermissionsContext grants all permissions, and authentication is handled locally with a simple email/password stored in AsyncStorage.

Dashboard

The mobile dashboard shows a sync status banner (online/offline with pending change count), stat cards (vehicles, fuel, service, mileage), and vehicle notifications with severity colours (danger for expired, warning for ≤30 days, info for ≤60 days). Notifications can be dismissed, cleared, or reset. The dashboard fires Android system notifications for critical items via the @notifee service.

Vehicles

The vehicle list supports search, status filter chips (All/Live/Sold/Scrapped), and a FAB button to add new vehicles. Each vehicle card shows the registration, make/model, mileage (with unit conversion), and an icon based on fuel type. Tapping a vehicle opens the detail screen with a header card, quick actions (Add Fuel / Add Service), cost summary grid, important dates with days-remaining indicators, key info list, and navigation to all record types.

Records & Forms

All record screens follow a consistent pattern: vehicle selector, card-based list with chips for metadata, and form screens for creating/editing. Each form supports receipt photo capture via camera or gallery (using react-native-image-picker). The QuickFuelScreen provides a streamlined one-tap fuel logging experience with auto-populated mileage.

The vehicle form has sections for Basic Info, Status (segmented buttons), Specifications, and Purchase Info. MOT forms include structured advisory and failure entry. The DVSA Vehicle Lookup screen lets you enter a registration and view specs plus full MOT history without creating any records.

Offline & Sync

The offline system is built around two core pieces: SyncContext and useOfflineData.

SyncContext monitors network connectivity via @react-native-community/netinfo. When the device is offline, mutations (create, update, delete) are queued in AsyncStorage. When connectivity returns, the queue is replayed with a 2-second debounce to avoid hammering the server. Each operation is retried up to 5 times. Operations that fail with a 4xx status code are automatically discarded (since the data is permanently invalid). Entity-to-endpoint mapping covers 10 entity types.

useOfflineData provides a cache-first data-fetching hook. On mount, it immediately returns cached data (if available and less than 24 hours old), then fetches fresh data from the network. If the network request fails, the cached data remains visible. This ensures the app always shows something, even in spotty connectivity.

Notifications

The NotificationService uses @notifee/react-native to create Android notification channels (vehicle-reminders and vehicle-urgent). It calculates notifications for live vehicles based on MOT expiry, insurance expiry, road tax expiry, and service due dates. Severity levels are: danger (expired or < 0 days), warning (≤ 30 days), and info (≤ 60 days). The top 5 most critical notifications are fired as system notifications.

Settings

The Settings screen provides comprehensive configuration:

  • Sync status - Shows online/offline state with a "Sync Now" button and pending count
  • Language selector - 20 languages with flag emojis and native names
  • Theme - Light, System, or Dark (segmented buttons)
  • Currency - 17 currencies (GBP, USD, EUR, JPY, etc.)
  • Distance unit - Miles or kilometres
  • Volume unit - Litres or gallons
  • Notification toggles - Service, MOT, and insurance reminder switches
  • Server info - Connected server URL
  • Clear Data & Disconnect - Reset the app to ServerConfigScreen
  • Logout

Administration

The admin panel is available to users with the ROLE_ADMIN role. It provides three main capabilities: user management, feature flag control, and vehicle assignment management. All admin operations are enforced server-side - the frontend simply hides the UI for non-admin users, but the API will reject unauthorised requests regardless.

User Management

From the Admin Users page, you can see every registered account along with their vehicle count, role, last login time, and active status. Available actions include:

  • Create user - Set up a new account with email, password, name, and role. Useful for onboarding family members or fleet drivers.
  • Toggle active - Disable or re-enable a user account. Disabled users cannot log in.
  • Force password change - Sets a flag that forces the user to choose a new password on their next login. Use this if you suspect a compromised account.
  • Manage roles - Promote a user to ROLE_ADMIN or demote back to ROLE_USER.

Feature Flags

Vehicle Manager has 49 feature flags organised by category. By default, all flags are enabled for everyone. Admins can override flags on a per-user basis - for example, disabling the Reports feature for a specific user, or restricting a user to only view fuel records and nothing else.

The Admin User Details page shows all 49 flags as categorised toggle switches. Changes are saved with a bulk update. You can also reset a user back to the system defaults with one click. Admin users always get all features enabled regardless of overrides.

The FeatureFlagSubscriber enforces these flags on every API request. If a disabled feature is accessed, the server returns 403 Forbidden. Authentication, admin, and lookup routes are exempted from flag checking.

Vehicle Assignments

Vehicle assignments let you share vehicles between users. This is useful in scenarios like:

  • A family sharing access to household vehicles
  • A fleet manager giving drivers read-only access to their assigned vehicles
  • A mechanic needing to add service records to customer vehicles

Each assignment has four granular permissions:

PermissionDefaultAllows
canViewtrueSee the vehicle and its records
canEdittrueModify vehicle details
canAddRecordstrueCreate fuel, service, and other records
canDeletefalseDelete the vehicle and its records

A user can be assigned to the same vehicle only once (enforced by a unique constraint). Assignments are managed through the Admin User Details page or directly via the API.

FAQ & Troubleshooting

General Questions

How do I make myself an admin?

Run this SQL against your database: UPDATE users SET roles = '["ROLE_USER","ROLE_ADMIN"]' WHERE id = 1; - replacing 1 with your user ID. There is no UI for the initial admin promotion since it would be a security risk.

How do I change the JWT token expiry time?

Edit config/packages/lexik_jwt_authentication.yaml and change the token_ttl value (in seconds). The default is 3600 (one hour). Restart the PHP container after changes.

What vehicle types are supported?

Five types: Motorcycle, Car, Van, Truck, and EV. Each type has its own set of consumable types, part categories, and security features.

How is depreciation calculated?

Four methods are available. The default is automotive_standard, which applies 20% depreciation in the first year and 15% per year thereafter - closely matching real-world vehicle depreciation patterns. You can also choose straight-line, declining balance, or double-declining balance.

What's the difference between parts and consumables?

Parts are permanent replacements - brake pads, air filters, bulbs, exhaust components. They're bought, installed, and that's it. Consumables are items that need regular, repeated replacement - engine oil, coolant, brake fluid, tyres, chain lube. Consumables have replacement-interval tracking, so the system can notify you when they're due.

What currencies are supported?

17 currencies: GBP (£), USD ($), EUR (€), JPY (¥), AUD, CAD, CHF, CNY, DKK, HKD, INR, KRW, NOK, NZD, PLN, SEK, and TRY. The currency selector is in Settings (mobile) or Preferences (web).

What languages are supported?

The web app supports 21 languages: Arabic, Czech, Danish, German, English, Spanish, Finnish, French, Hindi, Italian, Japanese, Korean, Dutch, Norwegian, Pirate 🏴‍☠️, Polish, Portuguese, Russian, Swedish, Turkish, and Chinese. The mobile app supports 20 of these (no Pirate).

Docker Issues

Containers won't start

Check that Docker has at least 4 GB of memory allocated (Docker Desktop → Settings → Resources). Ensure ports 3000, 8081, 3306, and 6379 are not already in use by other services. Run docker-compose logs for specific error messages.

MySQL won't connect

MySQL has a health check configured - it may take 30–60 seconds to become ready after starting. Check with docker-compose ps and wait for the MySQL container to show "healthy". Verify DATABASE_URL in your .env file points to the correct host (usually mysql inside Docker).

Redis connection refused

Ensure the Redis container is running and healthy: docker-compose ps redis. Check that REDIS_URL uses the hostname redis (not localhost) inside Docker. The default URL should be redis://redis:6379.

How do I view logs?

Use docker-compose logs -f php for backend logs, docker-compose logs -f nginx for request logs, or docker-compose logs -f frontend for React dev server output. Add --tail=100 to limit output.

How do I run migrations?

docker-compose exec php bin/console doctrine:migrations:migrate --no-interaction - though migrations typically run automatically via the PHP entrypoint script on container start.

How do I seed reference data?

Two commands: docker-compose exec php bin/console doctrine:fixtures:load --no-interaction for lookup data (vehicle types, makes, models, categories), then docker-compose exec php bin/console app:seed-feature-flags for the 49 feature flags.

API Issues

401 Unauthorized

Your JWT token has expired (tokens last 1 hour by default). Use the refresh endpoint: POST /api/auth/refresh with your refresh token in the body. The web app handles this automatically via the AuthContext interceptor.

403 Forbidden

Either a feature flag is disabled for your user, or you're trying to access an admin-only endpoint without ROLE_ADMIN. Check your effective features via GET /api/me and ask an admin to enable the relevant flag.

CORS errors

In development, CORS is set to allow all origins. In production, check the CORS_ALLOW_ORIGIN environment variable - it must match your frontend's URL exactly (including the protocol and port).

Upload fails

Check the file size against UPLOAD_MAX_BYTES (default 200 MB). Also verify the MIME type is allowed - the system rejects certain file types for security. The PHP container has a 200 MB upload limit and 1 GB memory limit.

OCR returns empty or garbage

Ensure Tesseract is installed in the PHP container (it's included in the default Dockerfile). OCR quality depends heavily on image quality - photograph receipts in good lighting, flat against a dark surface. Thermal receipts from petrol stations fade quickly, so photograph them promptly.

Mobile App Issues

App can't connect to server

On Android emulators, use 10.0.2.2 instead of localhost - the emulator routes that address to the host machine. Make sure the URL ends with /api. For physical devices, use your computer's local network IP (e.g., 192.168.1.x:8081/api).

Offline changes not syncing

Check your network connectivity first. The SyncContext waits 2 seconds after reconnection before syncing, and retries each operation up to 5 times. Open Settings and tap "Sync Now" to force immediate sync. If operations fail with 4xx errors, they're discarded as permanently invalid.

Notifications not appearing

System notifications require explicit permission on Android. Check your device's notification settings for the app. Only Android is supported - iOS notifications are not yet implemented. The app creates two channels: "vehicle-reminders" and "vehicle-urgent".

How do I switch from standalone to web mode?

Go to Settings → scroll to the bottom → tap "Clear Data & Disconnect". This wipes local data and restarts the app at the ServerConfigScreen, where you can enter a server URL. Note: standalone data is not migrated - back it up first if needed.

Import & Export Issues

Import fails

Ensure the JSON format matches the export format exactly. Common issues include duplicate VINs or registration numbers (the system enforces uniqueness), missing required fields, and malformed dates. The import is transactional - if anything fails, nothing is committed. Check the error response body for specifics.

ZIP import missing files

Attachment files within the ZIP must be in an "attachments" subfolder matching the export structure. If files are in the wrong location, the vehicle data will import successfully but the attachments will be skipped.

Export is slow for large collections

Large vehicle collections with many attachments take time to process. The system processes in batches of 25 vehicles with a 1 GB memory limit. ZIP exports are especially slow because all attachment files must be read and compressed. For very large collections, consider exporting without attachments first (JSON format).

Vehicle Manager Documentation · GitHub · Open source & free forever