← Back to the blog

Recipe: license-plate access control for a parking gate

Read plates off a gate camera, match them against a whitelist and blacklist, and fire signed plate events that open a barrier or log an entry. Open-source models, a per-app database, no cloud.

3 min read
Leer en español

Why LPR at the gate

A parking barrier or a private street gate has one question: is this plate allowed in? The LPR Studio App answers it from an ordinary camera — detect the plate, read it, match it against your lists, and emit an event your barrier controller or backend can act on. It runs entirely on open-source models, keeps a per-app history, and never calls a cloud plate service.

Architecture in words

LPR is a Studio App with a Python worker (fast-alpr: an ONNX detector plus OCR, all MIT-licensed). It samples the app's stream, reads plates, and on each accepted read emits plate.detected, plus plate.allowed if the plate is on your whitelist or plate.denied if it is on the blacklist. Events flow three ways: through the app's signed callbacks (webhook and MQTT), optionally to a direct callback URL (for a barrier opener), and to the dashboard's live feed. Reads are stored in a per-app lpr.db, with a snapshot JPEG per event.

1. Install and point at the gate camera

export BASE="https://your-server.example.com/api/v1"
export TOKEN="sk_...."

curl -X POST $BASE/apps/parking/plugins/lpr/install -H "Authorization: Bearer $TOKEN"

curl -X PATCH $BASE/apps/parking/plugins/lpr -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "enabled": true,
        "config": {
          "room": "gate-cam",
          "whitelist": "AB123CD, AA000AA",
          "blacklist": "ZZ999ZZ",
          "dedupeWindowSec": 10,
          "callbackUrl": "https://gate.example.com/open"
        }
      }'

room is required. Lists are comma- or newline-separated and matched case- and separator-insensitively.

2. Understand the events

On each read the worker emits plate.detected and, if it matches a list, plate.allowed or plate.denied (a plate on both lists is denied — the blacklist wins). Every event carries the same payload:

{ "plate": "AB123CD", "confidence": 0.94, "ts": 1751000000.1,
  "listMatch": "whitelist", "snapshot": "…/lpr/snapshots/1751000000100-AB123CD.jpg" }

Through the signed-callbacks path the body is wrapped as { event, app, room, ts, data } and signed with X-StreamHub-Signature: sha256=<hmac>. The optional direct callbackUrl gets a flat, unsigned body — convenient for a barrier controller on your LAN, but keep that endpoint private or gate it with a shared token.

3. Tune accuracy and rate

Config knobs (with defaults):

  • detectoryolo-v9-t-384-license-plate-end2end (balanced); 256 is fastest, 512 most accurate.
  • ocrModelcct-xs-v1-global-model (fast) or cct-s-v1-global-model (accurate).
  • confidence0.5 minimum OCR confidence.
  • fps2; dedupeWindowSec10 (the same plate inside the window emits once; a car idling re-emits every window, not per frame).
  • snapshotstrue; cudafalse.

4. Read history and wire the barrier

The dashboard app-tab shows recent reads via the authenticated live channel (GET /apps/parking/plugins/lpr/data). To open the barrier, have your controller listen on the direct callbackUrl and act on plate.allowed; log everything from the signed webhook into your own system; the per-app apps/parking/lpr/lpr.db keeps the full reads history for audits.

What you should see

A car pulls up; within a second or two plate.detected fires, and a whitelisted plate produces plate.allowed with a snapshot. Your gate opens; an unknown plate simply logs a plate.detected with listMatch: null; a blacklisted plate fires plate.denied.

Troubleshooting

  • Plates missed or misread — improve the camera angle and lighting on the plate, raise the detector to 512 and OCR to cct-s, or lower fps if the plate blurs at speed.
  • Repeated events for a parked car — that is dedupeWindowSec heartbeating; raise it if you only want one event per arrival.
  • Barrier opens on the wrong plate — remember the direct callback is unsigned; verify the plate in your controller, or consume the signed webhook instead.
  • Disk fills up — neither lpr.db nor the snapshots auto-prune; add a cron job on a busy gate.

Closing

Everything — models, plate history, snapshots — stays on your server, which matters for a residential or corporate site under privacy rules. If you want barrier hardware integration, multi-lane gates, or a resident self-service portal, that is a focused consulting build on top of the open-source LPR app.