9.6 KiB
UniFi Access Badge-In Dashboard
A Dockerised Flask + SQLite attendance dashboard that receives real-time door unlock webhooks from one or more UniFi Access Developer API controllers, resolves badge holders to real names, and displays a unified live attendance table with first/latest badge times, source controller, and ON TIME / LATE status.
Multi-controller: add as many UniFi Access controllers as the host can reach. Webhooks are auto-registered when you add a controller from the UI.
Requirements
- Linux host with Docker + Docker Compose (Unraid works great)
- One or more UniFi OS consoles running UniFi Access 1.9.1 or later
- Network reachability from the dashboard host to each controller on port 12445
- A Developer API token from each UniFi Access controller you plan to add
Step 1 — Open firewall port 12445 to each controller
The UniFi Access Open API runs exclusively on port 12445 (HTTPS, self-signed cert). The host running the dashboard must be able to reach each controller on that port.
In UniFi Network → Settings → Firewall & Security → Firewall Rules, add a LAN IN rule on each controller:
| Field | Value |
|---|---|
| Action | Accept |
| Protocol | TCP |
| Destination Port | 12445 |
| Source | the subnet your dashboard host lives on |
Verify from the dashboard host:
# Linux:
nc -zv 10.0.0.1 12445
# Windows PowerShell:
Test-NetConnection -ComputerName 10.0.0.1 -Port 12445
Step 2 — Generate a Developer API token on each controller
⚠️ This token is different from the UniFi OS / Network API token. Creating it in the wrong place will result in 401 Unauthorized errors.
- Open the UniFi OS console at
https://<controller-ip>in a browser. - Open the Access app (blue door icon).
- Go to Settings → General → Advanced → API Token.
- Click Create New, name it, enable all permission scopes, and pick a validity period.
- Click Create and immediately copy the token — it's only shown once.
Repeat for each controller you plan to add.
Step 3 — Clone the repo on the host
cd /mnt/user/appdata
git clone https://github.com/jasonMPM/unifi-access-dashboard.git unifi-access-dashboard
cd unifi-access-dashboard
Step 4 — Create your .env file
cp .env.example .env
nano .env
The UNIFI_* and WEBHOOK_SECRET values are optional. If set, they auto-create
a "Default" controller on first boot — handy for single-controller installs. You can
leave them blank and add every controller via the UI instead.
# Optional: seeds a "Default" controller on first boot
UNIFI_HOST=10.0.0.1
UNIFI_PORT=12445
UNIFI_API_TOKEN=YOUR_TOKEN_HERE
WEBHOOK_SECRET=
# Required
TZ=America/Chicago
DB_PATH=/data/dashboard.db
# Optional: override the URL the dashboard uses when registering webhooks
# DASHBOARD_BASE_URL=http://10.0.0.5:8000
Never commit
.envto git. It is listed in.gitignore.
Step 5 — Build and start the container
cd /mnt/user/appdata/unifi-access-dashboard
/usr/bin/docker compose up -d --build
The container will:
- Build the image from the local
Dockerfile - Start Flask on port 8000
- Create
/data/dashboard.dbinside the container (mapped to./data/on the host) - If env-var credentials are set, seed a "Default" controller and sync its users
- Schedule a user-cache refresh every hour for every enabled controller
Verify it's running:
/usr/bin/docker ps
/usr/bin/docker logs -f unifi-access-dashboard
Step 6 — Add controllers from the UI
Navigate to:
http://<HOST-IP>:8000/
Click the ⚙ Controllers button in the header. For each UniFi Access instance you want to receive events from, fill in:
| Field | Value |
|---|---|
| Name | Friendly label shown in the Source column (e.g. "Main Office", "Warehouse") |
| Host / IP | Controller IP, e.g. 10.0.0.1 |
| Port | 12445 (don't change unless your controller is non-standard) |
| Developer API Token | Token from Step 2 |
Click Add Controller. The dashboard will:
- Call the controller's
POST /webhooks/endpointswith this dashboard's URL. - Store the returned webhook secret so it can verify incoming events (HMAC-SHA256).
- Immediately sync the controller's user list to resolve names.
If the controller can't reach this dashboard at the URL shown in the form hint
(it uses window.location.origin by default), set DASHBOARD_BASE_URL in .env
and restart.
Per-controller actions in the modal:
| Action | Description |
|---|---|
| Test | Hits the controller's /users endpoint to confirm the token works |
| Sync | Pulls latest users from this controller right now |
| Enable / Disable | Pause ingestion + sync without deleting the controller |
| Remove | Deletes the webhook from the controller and wipes all its badge events from the dashboard |
Dashboard controls
| Control | Description |
|---|---|
| Date picker | Choose which day to view |
| Badged in by | Set your on-time cutoff (HH:MM) |
| Controller | Filter the table to one controller, or show All |
| Refresh | Reload the table |
| Sync Users | Pull latest users from every enabled controller |
| ⚙ Controllers | Add / manage controllers |
| Reset Day | Delete all badge records for the selected date (respects the Controller filter — testing only) |
Dashboard columns
| Column | Description |
|---|---|
| # | Row number |
| Name | Resolved display name from UniFi Access |
| Source | Controller this badge event came from |
| First Badge In | Earliest door entry for the day — never changes once set |
| Latest Badge In | Most recent entry — shows "— same" if only one badge event |
| Actor ID | First 8 characters of the UniFi user UUID |
| Status | ON TIME (green) or LATE (red) based on first badge vs cutoff |
The same physical person on two different controllers will appear as two rows (different controllers issue different user UUIDs). They're distinguishable by the Source column.
Updating from GitHub
cd /mnt/user/appdata/unifi-access-dashboard
git pull
/usr/bin/docker compose up -d --build
The SQLite database in ./data/ persists across rebuilds. On first start after
upgrading from a single-controller install, existing badge events are
automatically attached to the seeded "Default" controller — nothing to migrate
by hand.
API reference
All endpoints are unauthenticated by design — this app assumes a LAN-only deployment. Do not expose port 8000 to the internet without putting a reverse proxy with auth in front of it.
| Method | Path | Params / Body | Description |
|---|---|---|---|
POST |
/api/unifi-access/<controller_id> |
webhook body | Receives UniFi Access webhook for that controller |
POST |
/api/unifi-access |
webhook body | Legacy alias — routes to the oldest controller |
GET |
/api/first-badge-status |
date, cutoff, controller_id? |
Returns first + latest badge per user |
GET |
/api/controllers |
— | List configured controllers |
POST |
/api/controllers |
name, host, port, api_token |
Add a controller (also registers webhook) |
PATCH |
/api/controllers/<id> |
name?, enabled? |
Rename or enable/disable a controller |
DELETE |
/api/controllers/<id> |
— | Remove a controller (deletes webhook + its events) |
POST |
/api/controllers/<id>/test |
— | Test controller reachability + token |
POST |
/api/controllers/<id>/sync |
— | Sync one controller's user cache immediately |
GET |
/api/sync-users |
— | Sync every enabled controller |
DELETE |
/api/reset-day |
date, controller_id? |
Delete badge records for a date (optionally scoped to one controller) |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Add Controller fails with "webhook registration rejected" | Token invalid or wrong scope | Regenerate token in Access → Settings → General → API Token with all scopes enabled |
| Add Controller fails with a connection error | Host unreachable on port 12445 | Verify firewall rule from Step 1; nc -zv <ip> 12445 from the dashboard host |
| Events arrive but signature is rejected | Webhook secret missing or stale | Remove the controller and re-add it (the dashboard re-registers and gets a fresh secret) |
| Source column says "—" | Pre-migration row with no controller_id | Restart the container; the migration runs on every boot |
Names show as Unknown (xxxxxxxx...) |
Users not synced yet for that controller | Click Sync in the Controllers modal |
| Webhook URL stored in controller points to the wrong address | Browser's origin isn't reachable from the controller | Set DASHBOARD_BASE_URL in .env, remove + re-add the controller |
Port 12445 connection refused |
Firewall blocking port | Add LAN IN firewall rule in UniFi Network (Step 1) |
| Dashboard shows stale names after a user rename | Cache not refreshed | Click Sync Users or wait for the hourly auto-sync |
Security notes
.envis excluded from git via.gitignore— never commit it.- API tokens are stored in plaintext in the SQLite DB; the dashboard
assumes a LAN-only deployment. Filesystem permissions on
./data/dashboard.dbare the only thing protecting them. - All admin endpoints (
/api/controllers/*,/api/sync-users,/api/reset-day) are unauthenticated. Do not expose port 8000 publicly. If external access is required, place Nginx, Traefik, or Caddy with HTTPS + auth in front of port8000. - Each controller's
webhook_secretis enforced via HMAC-SHA256 on incoming events so spoofed webhook posts from outside the LAN are rejected.