npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hanoilab/zk-bridge

v0.1.8

Published

Pull-side bridge for ZKTeco attendance devices — polls over LAN and pushes events to any HTTP backend

Downloads

610

Readme


What is this?

ZKTeco fingerprint readers don't speak HTTP — they only accept TCP connections from inside the LAN, and the C-HR / HRIS backend lives in the cloud. ZK-Bridge sits in the office, polls the device on a schedule, and pushes attendance events to any HTTP API you point it at.

ZKTeco device                ZK-Bridge                  Your backend
┌─────────────────┐       ┌──────────────┐           ┌──────────────┐
│ Fingerprint /   │ ◄─TCP─┤  CLI + UI    │ ◄──HTTPS──┤  /push       │
│ face reader     │ 4370  │  SQLite      │           │  /ping       │
│ 192.168.x.y     │       │  Local LAN   │           │  Cloud / VPS │
└─────────────────┘       └──────────────┘           └──────────────┘
                          Outbound only — no port
                          forwarding or VPN needed

No vendor lock-in: any backend that exposes a JSON POST endpoint with a JWT auth header works. Bridge handles the LAN side.

Quick Start

1. Install globally

npm i -g @hanoilab/zk-bridge

2. Run it

zk-bridge start
[zk-bridge] started (PID 12345)
  Logs:  ~/.local/share/zk-bridge/zk-bridge.log
  Stop:  zk-bridge stop
  Tail:  zk-bridge logs -f

start detaches — closing the terminal won't stop the bridge. Useful one-liners:

zk-bridge status                 # is it running?
zk-bridge logs -f                # follow the log
zk-bridge stop                   # stop it

3. Open the admin UI

Visit http://localhost:7000, set the backend Push URL, paste the per-device JWT — bridge pushes attendance on the next cycle.

Features

Web Admin UI

  • Single-user login (bcrypt + signed-cookie session, 7-day TTL)
  • Configure backend Push / Ping URL + poll interval
  • Add devices manually or via LAN scan
  • Per-device events with cursor position + push status badge
  • Cycle history with status, timing, error message, filter by device

LAN Discovery

  • Auto-scan /24 subnet for ZKTeco devices on TCP 4370
  • Identify each candidate over the ZK protocol (model, serial)
  • One-click "Add as device" pre-fills name + host

Multi-Device

  • One bridge polls many devices on a single schedule
  • Per-device cursor, queue, audit log, error state
  • Enable / disable individually without removing config

Offline Tolerance

  • Queues events to local SQLite when the backend is unreachable
  • Drains the queue on the next online cycle
  • Cursor advances even on partial failure — no data loss, no double-send

Idempotent Push

  • Backend dedupes by (deviceId, eventLogId) — replay is a no-op
  • Batches events at 200/request to stay under common body-parser limits
  • JWT version counter for revocation (regenerate on the backend → old JWTs rejected immediately)

Auto-Start on Boot

  • One-click toggle in the System page registers a:
    • systemd unit (Linux)
    • Scheduled Task (Windows)
    • launchd plist (macOS)
  • Restart=on-failure so a crashed cycle never takes the bridge down

Cross-platform

  • Linux, macOS, Windows
  • Node 20+ — no native build needed (sqlite3 ships prebuilds)

CLI

zk-bridge start                  # Start as background daemon (writes PID + log)
zk-bridge stop                   # Stop the daemon
zk-bridge restart                # Stop + start
zk-bridge status                 # PID, uptime, log path
zk-bridge logs -f                # Follow the log
zk-bridge logs -n 200            # Last 200 lines
zk-bridge run                    # Run in the foreground (debug / systemd / Docker)
zk-bridge poll-once              # Run a single cycle then exit
zk-bridge reset-user             # Forgot-password recovery
zk-bridge recent-events          # Print last N events from a device
zk-bridge upgrade [tag]          # Self-update via npm
zk-bridge --help
zk-bridge --version

start detaches from the launching shell — Ctrl+C in that terminal does NOT kill the daemon. Use zk-bridge stop. Logs go to <DATA_DIR>/zk-bridge.log.

Environment overrides (otherwise default):

PORT=8080 BIND_HOST=0.0.0.0 zk-bridge start
DATA_DIR=/var/lib/zk-bridge zk-bridge start

How It Works

Every cycle (default 5 min):

  1. List enabled devices in local SQLite.
  2. For each: open ZK socket, fetch attendance log, take the last N events.
  3. Drain the offline queue, then push new events to the backend in batches of 200.
  4. Advance the cursor, write a cycle_log row.

State that lives locally:

~/.local/share/zk-bridge/zk-bridge.db   (Linux default)
%APPDATA%\zk-bridge\zk-bridge.db        (Windows)
~/Library/Application Support/zk-bridge/zk-bridge.db  (macOS)

  ├── users          (single admin row)
  ├── config         (push URL, ping URL, poll interval, session secret)
  ├── devices        (host, port, JWT, cursor, last status)
  ├── event_queue    (offline-pending events)
  └── cycle_log      (per-device cycle history, rotated to last 1000)

Backend Contract

Two HTTP endpoints. Configure their full URLs in the UI — the bridge appends nothing.

Push (required)

POST <push-url>
Content-Type: application/json

{
  "token": "<JWT signed by backend>",
  "events": [
    {
      "eventLogId": "12345",
      "employeeCode": "EMP-0001",
      "timestamp": "2026-05-07T08:23:45.000Z",
      "type": "IN"
    }
  ]
}

The backend should:

  • Verify the JWT (signature + expiry / version).
  • Resolve the device row from the JWT payload.
  • Dedupe by (deviceId, eventLogId) — replays are safe.
  • Persist or normalize the events as needed.

Response shape isn't enforced — bridge only checks the HTTP status (2xx = success, anything else = retry / queue).

Ping (optional)

POST <ping-url>
Content-Type: application/json

{ "token": "<JWT>" }

Used by the Connect button to verify the JWT + URL without sending events. If you don't expose a separate ping endpoint, leave the field blank — the bridge falls back to a push with an empty events array (your backend should respond 4xx for that case, which the bridge interprets as "auth + URL OK").

Reference implementation

See c-hr backendapps/backend/src/apps/attendance/attendance-device/ is a NestJS module that implements the contract end-to-end, including JWT version revocation and orphan-event reconcile.

Self-Hosting

git clone https://github.com/nguyendinhphongdx/zkteco-bridge.git
cd zkteco-bridge
pnpm install
pnpm build
pnpm start

Or install your local checkout as the global CLI:

npm install -g .
zk-bridge start

Auto-start (production)

Three options — pick one, otherwise two processes will fight for port 7000:

  • Built-in toggle (recommended) — System → Auto-start on boot registers a systemd / Windows Task / launchd entry pointing at the global CLI binary.

  • PM2

    pm2 start "$(which zk-bridge)" --name zk-bridge -- start
    pm2 startup && pm2 save
  • Docker — see docker-compose.yml. Bind-mount ./data:/app/data to persist SQLite across rebuilds.

Environment Variables

| Variable | Default | Description | |----------|---------|-------------| | PORT | 7000 | Admin UI HTTP port | | BIND_HOST | 127.0.0.1 | Listen address. Set 0.0.0.0 to allow LAN access | | DATA_DIR | OS-standard user data dir | Override SQLite + admin login location | | PUSH_URL | (none) | First-run seed only — bridge stores it in SQLite then ignores env | | PING_URL | (none) | First-run seed only | | POLL_INTERVAL_MIN | 5 | First-run seed only — minutes between cycles |

DATA_DIR resolution order on every start:

  1. DATA_DIR env var (always wins)
  2. ./data/ next to cwd, if it exists (Docker bind mount, dev workflow)
  3. Globally installed: OS-standard user data dir
  4. ./data/ next to cwd (dev fallback)

Troubleshooting

| Symptom | Likely cause / fix | |---------|-------------------| | ETIMEDOUT <ip>:<port> on Connect | Bridge can't reach the backend Push URL. curl <url> from the bridge host should work. | | HTTP 401 Invalid token | JWT was regenerated on the backend, or the device row was deleted. Re-paste the token. | | Socket closed unexpectedly | The ZK device only allows 1 active connection — another tool / cycle is holding it. Wait for the next cycle. | | port open but ZK probe fail in scan | Same — device is busy. Try AddConnect after a minute. | | Dashboard shows "never run" | Push URL not set, or no devices configured. Check API settings + Devices. | | Events arrive late | Lower the poll interval in API settings (min 1 min). |

Every console line is prefixed with an ISO timestamp so logs from pm2 logs / journalctl -u zk-bridge / docker compose logs line up:

[2026-05-07T08:23:50.747Z] [poll] "Front gate" pulled 2915 from ZK in 5291ms

Tech Stack

| Layer | Technology | |-------|-----------| | Runtime | Node.js 20+ | | Local DB | SQLite via sqlite3 + Sequelize | | HTTP server | Hono + @hono/node-server | | ZK protocol | Custom client (src/zklib) — TCP raw, chunked streaming for large logs | | Auth | bcryptjs (admin) + JWT (per-device) | | HTTP client | axios | | Scheduler | node-cron | | Build | TypeScript |

License

MIT © HanoiLab