Skip to content

About

Self-hosted label printer hub for Brother PT-Series and QL-Series — multi-printer queue management, Snipe-IT/Grocy/Spoolman integration, plugin-based architecture, PWA-ready

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

261 Commits

Folders and files

Label Printer Hub

CI CodeQL codecov CLA assistant License: AGPL v3 Conventional Commits semantic-release GitHub Issues

Self-hosted multi-printer hub for Brother PT-Series and QL-Series label printers. Pull-mode (user scans barcode) and push-mode (Spoolman/Grocy webhooks). Integrates with Snipe-IT, Grocy, Spoolman. Plugin-based architecture for additional printer models. PWA-ready for smartphone use.

Status

Early development. See open issues for progress and the master tracking issue #22 for the phase roadmap.

This project is being designed against the Brother PT-E550W/P750W/P710BT Raster Command Reference v1.02 and QL-800/810W/820NWB Raster Command Reference. Hardware tested: Brother PT-P750W (verified). Brother QL-820NWBc (in progress).

Features (planned)

  • Multi-printer support via plugin architecture (PT-Series and QL-Series, more on request)
  • Pull-mode: Open the web UI on your phone, scan a barcode, hit print
  • Push-mode: Spoolman/Grocy webhook → automatic label print
  • App integrations: Snipe-IT (asset tags), Grocy (product labels), Spoolman (3D-print spool labels)
  • Print queue with pause/resume/cancel/retry/priority operations
  • Live status pages per printer via Server-Sent Events (no page reload)
  • Tape detection via Brother native status block (no manual configuration)
  • PWA-installable for smartphone use
  • Pluggable: drop a new printer_models/your_model.py to add a new device

Tech Stack

One container: a single backend process serves the REST API, the live event stream and the web UI (ADR 0015). Printers are added in the UI (Settings > Printers), not through environment variables.

  • Backend (label-printer-hub-backend, the only image): Python 3.12+, FastAPI, SQLModel (SQLite), asyncio
  • Printer protocols: nbuchwitz/ptouch (PT-Series), pklaus/brother_ql (QL-Series), pysnmp for status polling; an own IPP client over httpx for page printers (ADR 0020), with reportlab drawing the PDF pages
  • Web UI (built into the backend image from frontend/): React + TypeScript, Vite, PWA (manifest + service worker + camera scanning) — ADR 0016
  • Container: Docker (multi-stage, one image), GHCR + Docker Hub publishing, multi-arch (amd64 + arm64)
  • CI/CD: GitHub Actions, semantic-release, Dependabot

Container images and tags

Every stable release publishes to GitHub Container Registry (GHCR) and Docker Hub with this tag scheme:

Tag Example for 1.0.0 Use when
1.0.0 exact version You want full reproducibility
1.0 latest patch in 1.0.x Auto-update bug fixes
1 latest minor.patch in 1.x.x Stay on major version, get features
latest most recent stable You're fine with anything new

Pre-releases (1.0.0-rc.1 etc.) publish only the full version tag — never latest, <major>, or <major>.<minor> — so a pre-release can never silently become the default.

Both registries receive identical multi-arch images (linux/amd64, linux/arm64).

Quick Start

See examples/README.md for sample compose files (standalone / Traefik / Pangolin / Caddy). Upgrading from the two-container release: migration notes.

git clone https://github.com/strausmann/Label-Printer-Hub.git
cd Label-Printer-Hub/examples
cp .env.example .env
# Required: PRINTER_HUB_SECRET_KEY (openssl rand -base64 32 | tr '+/' '-_')
#           PRINTER_HUB_PUBLIC_URL (the address you open the hub at)
$EDITOR .env
docker compose -f compose.standalone.yml up -d

curl http://localhost:8000/healthz
curl http://localhost:8000/api/info
# Open http://localhost:8000/ and sign in. A fresh install has no admin key:
# either set PRINTER_HUB_BOOTSTRAP_ADMIN_KEY (one-time key, 32+ characters) or run
docker exec label-printer-hub-backend printer-hub admin create-key --scope admin --name first-admin
# then add your printers under Settings > Printers.

To build and run the hub from source without any printer hardware, use the smoke-test compose file (throw-away keys, one container, built from the local tree):

export PRINTER_HUB_SECRET_KEY="$(openssl rand -base64 32 | tr '+/' '-_')"
docker compose -f dev/docker-compose.smoke.yml up --build
# UI and API on http://localhost:8000
curl http://localhost:8000/healthz

Other containers in the same stack can call the hub directly with an API key: using the hub from other services.

For the REST API alone, without the web UI, see examples/compose.backend-only.yml.

REST API surface

Method Path Purpose Body
POST /print Submit a print job PrintRequest (see below)
GET /jobs/{job_id} Poll job status (includes live SNMP block while printing) —
POST /jobs/{job_id}/resume Resume a job paused by tape mismatch (after the user changed the tape physically) —
POST /printer/resume Resume the printer queue after a recoverable error halted it (tape empty / cover open / offline) —
GET /healthz Liveness probe for orchestrators —
GET /readiness Readiness probe — deep check for reverse-proxy routing —

Health Probes

The backend exposes two HTTP probes with different semantics:

Endpoint Purpose What it answers
GET /healthz Liveness — Docker / Kubernetes container restart signal "the process and the event loop are alive"
GET /readiness Readiness — reverse-proxy routing signal "the process can serve traffic right now": database connectable, alembic at head, templates seeded, runtime printer matches DB, SNMP probe fresh, queue worker alive, SSE bus capacity ok

/readiness returns HTTP 200 with status of ready (all checks ok) or degraded (non-critical checks failing — still routable), and HTTP 503 with not-ready when a critical check (database, alembic, template_seed) fails.

Pangolin's targets[0].healthcheck.path can use /readiness for deep checks instead of /healthz; Docker container healthchecks should stay on /healthz to avoid restart loops on transient DB failures.

See docs/superpowers/specs/2026-05-17-phase-7b-foundation-design.md for the full check list and rationale.

POST /print request body

{
  "template_id": "qr-only-12mm",         // id from app/seed/templates/<id>.yaml
  // Exactly one of `lookup` or `data` is required.
  "lookup":  { "app": "snipeit", "identifier": "123" },
  "data":    { "title": "Asset 123",
               "primary_id": "ASSET-123",
               "qr_payload": "https://snipe.example/assets/123",
               "secondary": ["optional", "extra lines"] },
  "options": { "copies": 1,              // 1..10, default 1
               "auto_cut": true,
               "high_resolution": false },
  // Default "fail" → synchronous 409 + error_detail{expected_mm, loaded_mm}.
  // "queue"        → 202 + job_id; job lands in PAUSED until the user POSTs
  //                  /jobs/{id}/resume after physically swapping the tape.
  "on_tape_mismatch": "fail"
}

Synchronous error codes (POST /print)

HTTP error_code When
404 template_not_found unknown template_id
409 tape_mismatch loaded tape ≠ template tape, on_tape_mismatch="fail"
409 tape_empty preflight detects no media
409 printer_cover_open preflight detects cover open
502 integration_lookup_failed integration plugin raised
503 printer_offline SNMP preflight could not reach the printer

tape_mismatch responses include error_detail: {expected_mm, loaded_mm} so the client can build a "swap the tape" dialog.

Environment variables

Full reference lives in backend/.env.example. The most-used variables grouped by purpose:

Variable Default Purpose
PRINTER_HUB_SECRET_KEY required Master key for secrets stored in the database (base64url of 32 random bytes; openssl rand -base64 32 | tr '+/' '-_')
PRINTER_HUB_PUBLIC_URL required The address people open the UI at; decides cookie security (Secure and __Host- prefix when https) and the allowed Origin
PRINTER_HUB_BOOTSTRAP_ADMIN_KEY empty One-time key (32+ characters) that signs you in while no admin key exists; empty disables it
PRINTER_HUB_DATABASE_URL sqlite+aiosqlite:////data/printer-hub.db Database
PRINTER_HUB_SSO_TRUST_TOKEN empty Shared secret the proxy sends as X-Pangolin-Token; without it Remote-User is ignored
PRINTER_HUB_TRUST_LEGACY_PANGOLIN_USER false Opt in to the legacy X-Pangolin-User header
PRINTER_HUB_TRUST_PANGOLIN_BASIC_BYPASS false Opt in to the Authorization: Basic bypass credential
PRINTER_HUB_WEBHOOK_API_KEY empty Bearer for inbound integration webhooks (≥ 32 chars; generate with openssl rand -hex 32)
PRINTER_HUB_SNIPEIT_URL empty Snipe-IT base URL
PRINTER_HUB_SNIPEIT_API_KEY empty Snipe-IT bearer token
PRINTER_HUB_GROCY_URL empty Grocy base URL
PRINTER_HUB_GROCY_API_KEY empty Grocy API key
PRINTER_HUB_SPOOLMAN_URL empty Spoolman base URL (no auth)
PRINTER_HUB_SERVER_PORT 8090 Internal port (overridden to 8000 in the container — see Dockerfile)
PRINTER_HUB_LOG_LEVEL INFO DEBUG / INFO / WARNING / ERROR

All variables share the PRINTER_HUB_ prefix and map 1:1 to the Settings model in backend/app/config.py. A variable set in the environment wins over the stored value and locks the field in the UI (D4). Printers have no variables: they are managed in Settings > Printers.

Documentation

In this repository (engineering — change with code):

On the wiki (tutorials and platform recipes — community-friendly):

Live API reference (when running): /openapi.json and /docs (interactive reference, Scalar, served by the hub itself) — see ADR 0011.

Contributing

Contributions are welcome — especially printer model plugins. See CONTRIBUTING.md for the workflow (Conventional Commits, TDD, semantic-release).

Trademarks and disclaimer

Brother, P-touch, PT-Series, and QL-Series are trademarks or registered trademarks of Brother Industries, Ltd. All other trademarks are the property of their respective owners.

This project is not affiliated with, endorsed by, or sponsored by Brother Industries, Ltd. It is an independent open-source project that interoperates with Brother label printers via documented protocols (Brother Raster Command Reference, IEEE 802.3, RFC 3805 SNMP Printer-MIB, RFC 8011 IPP).

"Brother" is used in this README solely for the purpose of describing the hardware that this software is compatible with. No commercial use of the Brother trademarks is intended.

License

Label Printer Hub is licensed under the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later) — see LICENSE.

Releases up to and including 1.0.3 are MIT; from 1.1.0 on AGPL-3.0-or-later. Anyone who received an earlier release keeps the MIT terms for that release.

Why the change:

  • The hub depends on brother_ql, which is GPL-3.0-or-later, so the combined work is distributed under terms at least as strong as GPL-3.0.
  • The hub is a network service. Section 13 of the AGPL gives the people who use a modified version over a network the right to its source.

If you run a modified version for others, set HUB_REPO_URL to where its source is published — the About page links there. Unmodified releases link to the exact tag of the running version.

Acknowledgements

About

Self-hosted label printer hub for Brother PT-Series and QL-Series — multi-printer queue management, Snipe-IT/Grocy/Spoolman integration, plugin-based architecture, PWA-ready

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages