Open-source ALPR data explorer β its just a Fluke!
A web application for searching, importing, and managing ALPR (Automated License Plate Reader) data in the Twin Cities metro area (and beyond)!
- Backend: PocketBase v0.25 (hosted on PikaPods)
- Frontend: React + Vite (hosted on Vercel)
- Import: Python scripts via
uv
- Node.js (v18+)
- Python 3.10+ and
uvpackage manager
Since the pocketbase binary isn't tracked in version control, it must be downloaded manually for a fresh local deployment.
- Download PocketBase v0.25+ for your system architecture from the PocketBase Releases page.
- Extract the
pocketbasebinary and place it inside the./backend/directory.
Before starting the frontend, you must initialize the local database schema and create a default superuser.
# From the project root, create the local superuser (initializes pb_data)
./backend/pocketbase superuser upsert admin@local.dev admin123456
# Start PocketBase (keep it running in this terminal)
./backend/pocketbase servePocketBase will run on http://127.0.0.1:8090.
Once PocketBase is running, open a new terminal tab and run the unified schema script to populate collections, API rules, and SQL views:
POCKETBASE_URL=http://127.0.0.1:8090 \
PB_ADMIN_EMAIL=admin@local.dev \
PB_ADMIN_PASS=admin123456 \
uv run scripts/setup-schema.pycd frontend
npm install
npm run devThe frontend will run on http://localhost:5173.
Admin scripts use Python and uv.
| Script | Purpose |
|---|---|
uv run scripts/import_csv.py <file.csv> |
Import data via CLI |
uv run scripts/backup.py |
Create a snapshot backup |
Fluke is designed to be hosted cheaply using PikaPods (backend) and Vercel (frontend).
Important: The PocketBase superuser account (used to manage the database) is completely separate from the Fluke app user account (used to log into the Fluke web UI). You will create both.
- Sign up at pikapods.com.
- Click Add Pod β search for PocketBase (v0.25.x). The smallest tier is fine.
- Once the pod is running, note your pod URL (e.g.,
https://your-pod.pikapods.net).
Create your PocketBase superuser (PikaPods v0.23+ process):
PikaPods does not prompt you to create an admin β instead, the one-time setup URL is hidden in your pod logs.
- In the PikaPods dashboard, click your pod β More β Show Logs
- Look for a line containing a URL that starts with:
followed by a long JWT token.
http://0.0.0.0:8090/_/#/pbinstal/ - Copy that URL and replace
http://0.0.0.0:8090with your actual pod URL:https://your-pod.pikapods.net/_/#/pbinstal/<jwt-token> - Open that modified URL in your browser and create your superuser account.
- Save these credentials somewhere safe β you will need them for every admin operation.
Update the Application URL:
Once logged into your PocketBase admin panel (https://your-pod.pikapods.net/_/):
- Go to Settings β Application
- Change Application URL from
http://localhost:8090tohttps://your-pod.pikapods.net - Click Save changes
Run these two scripts locally, pointing them at your live pod.
Run the unified initialization script locally, pointing it at your live pod. This script securely provisions the entire 3-tier database structure, creates all SQL views, and applies strict API rule assignments sequentially.
# Safely initialize the unified 3-tier schema and security rules
POCKETBASE_URL=https://your-pod.pikapods.net \
PB_ADMIN_EMAIL=your-superuser@email.com \
PB_ADMIN_PASS=your-superuser-password \
uv run scripts/setup-schema.pyWhen successful, you will see output confirming collections like vins, vehicles, sightings, and enhanced_vin_stats have been created or updated. If you see repeated API errors, double-check your credentials and ensure the POCKETBASE_URL env var string is completely accurate without a trailing slash.
The PocketBase superuser is only for database administration. You need a separate user account to log into the Fluke web UI.
- In your PocketBase admin panel β Collections β users β New record
- Fill in:
- username β e.g.
admin(this is what you type into the Fluke login screen) - email β your email address
- password + passwordConfirm β choose a secure password
- role β select
admin
- username β e.g.
- Save the record.
β οΈ Critical: Vite bakes environment variables into the bundle at build time. TheVITE_POCKETBASE_URLvariable must be set in Vercel before the first deploy, or the app will point tolocalhostand logins will fail.
-
Push this repo to GitHub.
-
Sign into vercel.com and click Add New β Project.
-
Import your GitHub repo. Select GitHub as the Git provider.
-
In the project configuration:
- Find Root Directory under Build and Output Settings and set it to
frontend - Under Environment Variables, add before clicking Deploy:
Key Value VITE_POCKETBASE_URLhttps://your-pod.pikapods.net - Find Root Directory under Build and Output Settings and set it to
-
Click Deploy.
If you forget the env var and deploy first, add it in Settings β Environment Variables afterward, then go to Deployments β Β·Β·Β· β Redeploy to trigger a fresh build.
| Check | Expected result |
|---|---|
| Visit your Vercel URL | Fluke login page appears |
| Log in with your app user | Redirected to search page |
| DevTools console has no errors | No 127.0.0.1 or CORS errors |
| Admin β Records | Collections visible, table loads |
| Admin β Upload CSV | Upload and import works |
Upload ALPR .csv files using the CSV Upload tab in the Fluke admin dashboard, OR via CLI:
POCKETBASE_URL=https://your-pod.pikapods.net \
PB_ADMIN_EMAIL=your-superuser@email.com \
PB_ADMIN_PASS=your-superuser-password \
uv run scripts/import_csv.py ./data/your-file.csvEvery git push to main will automatically trigger a new Vercel deploy. No action needed.
For PocketBase schema changes, simply re-run the unified setup-schema.py script against the production pod URL to gracefully apply new collections or rule modifications.
Fluke runs a nightly job that re-checks every known plate against the defrostmn.net database and updates any vehicles whose ICE status has changed. When an admin next logs in, a banner shows which plates changed.
-
GitHub Actions triggers at 4:00 AM CST (
0 10 * * *UTC) every night -
scripts/ice_refresh.pyfetches all plates from PocketBase -
Each plate is checked against
defrostmn.net/plates/lookup -
The defrost status is normalized to one of four values:
Defrost status string Written to DB searchableAdmin modal Confirmed ICE/CONFIRMEDYtrueβ Yes Highly suspected ICEHStrueβ Yes Cleared - NOT ICE(anyClearedβ¦prefix)Cfalseβ Silent Anything else / not found Nfalseβ Silent -
Escalations (
Y/HS): all sightings updated, vehiclesearchableset totrue, change logged toice_change_log -
Clearances (
C): all sightings updated, vehiclesearchableset tofalse, noice_change_logentry (no modal) -
On next admin login β dismissible notification banner for escalations only
Add these four secrets to GitHub β your repo β Settings β Secrets and variables β Actions β New repository secret:
| Secret name | Value |
|---|---|
POCKETBASE_URL |
https://your-pod.pikapods.net |
PB_ADMIN_EMAIL |
Your PocketBase superuser email |
PB_ADMIN_PASS |
Your PocketBase superuser password |
DEFROST_PASSWORD |
The defrost API shared password |
Go to GitHub β Actions β Nightly ICE Refresh β Run workflow to trigger a manual run and verify the output before relying on the schedule.
Edit .github/workflows/ice-refresh.yml and change the cron expression:
- cron: '0 10 * * *' # 4:00 AM CST β adjust as neededEvery run (success or failure) is logged in GitHub β Actions β Nightly ICE Refresh. The output shows:
- How many plates were checked
- How many changed
- Any errors or skipped plates