An open-source, privacy-first license plate scanner with a personal watchlist.
PlateWatch turns your phone into a real-time license plate reader. Point the camera at traffic, and it reads plates using on-device OCR. Keep a personal watchlist of plates you care about — when one is spotted, the screen flashes red and you get a local notification.
Everything runs entirely on your device. No cloud APIs, no accounts, no analytics, no data collection. Your watchlist and everything the camera sees never leave your phone.
This is a proof of concept. It works, but expect rough edges — and read the responsible use section before pointing it at the world.
- 📷 Live scanning — full-screen camera preview with OCR running at ~2.5 fps (throttled to be kind to your battery)
- 🔍 Plate extraction — filters OCR output down to plate-shaped text (2–8 alphanumeric characters, US-style formats)
- 📋 Personal watchlist — add plates manually; they're normalized (uppercased, spaces/hyphens stripped) so
abc-1234matchesABC1234 - 🚨 Match alerts — pulsing red border, the matched plate front and center, and a local notification
- 🔕 Alert deduplication — the same plate won't re-alert within a 60-second window
- 🔒 On-device only — ML Kit text recognition runs locally; storage is a local Hive box
| Scanner | Match alert | Watchlist |
|---|---|---|
| coming soon | coming soon | coming soon |
Grab the APK from the latest release and install it (you'll need to allow installs from unknown sources). It's a universal APK, so it works on any Android 7.0+ device.
iOS has no sideloading — build from source with Xcode (see below).
- Flutter 3.32+ (stable channel)
- Android: a physical device recommended (emulator cameras won't read real plates); minSdk follows Flutter's default (24)
- iOS: Xcode 15+, iOS 15.5+ (required by ML Kit), a physical device
git clone https://github.com/flutterWithChris/platewatch.git
cd platewatch
flutter pub get
flutter runOn first launch the app asks for camera permission (and notification permission on Android 13+ / iOS). If you decline, the scanner screen shows a prompt with a shortcut to system settings.
- Open the watchlist (list icon, top right) and add a plate — your own car is a good test subject.
- Go back to the scanner and point the camera at the plate.
- You should see detected text appear in the bottom overlay, then the red match alert and a notification.
flutter testThe plate extraction and matching logic is pure Dart and fully unit-tested — no device needed.
camera frames (~30 fps)
└─ throttle to ~2.5 fps scanner_screen.dart
└─ ML Kit text recognition services/ocr_service.dart
└─ extract plate shapes services/plate_extractor.dart
└─ match vs watchlist services/plate_matcher.dart
├─ overlay + red flash
└─ local notification services/notification_service.dart
lib/
├── main.dart # bootstrap: Hive, services, orientation lock
├── app.dart # MaterialApp + theme
├── models/
│ └── watchlist_entry.dart # Hive model (hand-written adapter)
├── screens/
│ ├── scanner_screen.dart # camera lifecycle + frame pipeline
│ └── watchlist_screen.dart # add / swipe-to-delete plates
├── services/
│ ├── ocr_service.dart # ML Kit wrapper + CameraImage conversion
│ ├── plate_extractor.dart # normalization + candidate filtering (pure Dart)
│ ├── plate_matcher.dart # watchlist matching + 60s alert cooldown
│ ├── watchlist_service.dart # Hive-backed watchlist store
│ └── notification_service.dart
└── widgets/
├── detection_overlay.dart # live "what the OCR sees" chips
├── match_alert_overlay.dart
└── camera_permission_view.dart
- Plate filtering is heuristic. A candidate is any 2–8 character alphanumeric string after normalization. The live overlay additionally requires ≥4 chars and at least one digit to cut down on street-sign noise — but watchlist matching checks all candidates, so short or all-letter vanity plates still alert. Tune these rules in
plate_extractor.dart. - The camera is torn down when the app leaves the foreground and rebuilt on resume — no background processing (yet, see roadmap).
- No state-management package. Services are plain classes injected through constructors;
WatchlistServiceis aChangeNotifier. If the app grows, swapping in provider/riverpod is mechanical. - Alert cooldown state is in-memory and resets when scanning restarts. A persistent match history is a roadmap item.
Things this architecture anticipates but the POC intentionally doesn't build:
- Crowdsourced / community watchlists with subscribe–unsubscribe (Supabase or Firebase) —
⚠️ gated on the responsible use design principles: opt-in vehicle-incident lists (e.g., stolen-vehicle reports) only, never lists that track people - Match history log with timestamps and GPS coordinates
- Plate image capture on match
- Watchlist export / import
- Background scanning mode
- Non-US plate format profiles
The seams are already in place: all plate sources funnel through WatchlistService, and every "a match happened" decision goes through PlateMatcher — history, image capture, and GPS tagging all hook in there.
PlateWatch is a neutral tool built for legitimate uses: spotting a stolen vehicle from a community alert, watching for a car you own, neighborhood-watch coordination. License plate surveillance carries real privacy implications — tracking individuals without consent may be illegal where you live, and it's a bad thing to do regardless. Know your local laws. Don't build databases of other people's movements.
Know your state law. Several US states restrict or prohibit private use of automated license plate readers outright — Arkansas, Maine, and New Hampshire effectively limit ALPR use to law enforcement. See the NCSL summary of state ALPR statutes and check your own jurisdiction before using this app.
Contributions must preserve these, in this order of priority:
- On-device only. The core app makes no network calls — no cloud OCR, no analytics, no telemetry, no accounts.
- No shared or supplied watchlists of people. Watchlists are personal, entered by the user, about vehicles they have a legitimate reason to watch. Never ship a list, and never make a person the unit of tracking.
- Match history, if ever built, stays local — with a short retention default and a one-tap purge.
- Alerts point to reporting, not confrontation. If a future feature acts on a match (e.g., a stolen-vehicle alert), it should direct the user to report to police — never to approach or follow anyone.
This project is meant to be forked and extended. PRs welcome — keep the on-device, no-cloud promise intact for the core app, and keep the plate pipeline (plate_extractor.dart, plate_matcher.dart) covered by unit tests.