Skip to content

thatdeercodes/parcelwatch

Repository files navigation

Parcelwatch

CI License: AGPL v3 Go

Self-hosted, privacy-first package tracking. Parcelwatch follows your shipments across multiple carriers, alerts you when their status changes, and keeps every tracking number, route, and delivery photo on your own server.

Contents

Why

Most tracking sites want your shipment data, your email, and your attention. Parcelwatch keeps all of that on hardware you control. Point it at the carrier APIs you already have access to, add your tracking numbers, and get notified through ntfy or email when something moves.

Features

  • Multi-user accounts with sessions, CSRF protection, and login rate limiting.
  • Per-user dashboard with live status refresh and per-package detail pages.
  • Background poller that detects status changes and triggers notifications.
  • Notifications through ntfy and email (SMTP), with the servers set by the admin and the topic or address chosen per user.
  • Opt-in route history with a self-hosted map, plus optional delivery photos.
  • Encryption at rest for credentials, tracking numbers, route history, and photos.
  • Admin controls for registration, carrier credentials, SMTP, the map, and updates.
  • Optional update checks against GitHub releases, off by default.
  • Self-hosted front-end assets (including Leaflet), so no third-party CDN is required.

Supported carriers

Carrier Auth Credentials needed
USPS OAuth2 Client ID and client secret
UPS OAuth2 Client ID and client secret
FedEx OAuth2 Client ID and client secret
Royal Mail OAuth2 Client ID and client secret
DHL API key API key
PostNL API key API key

Carriers are configured by an administrator and stay disabled until valid credentials are supplied.

Requirements

  • Docker to run the container image.
  • Carrier API credentials for whichever carriers you want to track.

Quick start

Parcelwatch is distributed as a container image. Run it with a volume mounted at /data so the database and its encryption key survive restarts:

docker run -d \
  --name parcelwatch \
  -p 8080:8080 \
  -v parcelwatch-data:/data \
  -e PARCELWATCH_URL="https://parcels.example.com" \
  ghcr.io/thatdeercodes/parcelwatch:latest

Then open http://localhost:8080 and register the first account, which becomes the administrator.

Multi-arch images (amd64 and arm64) are published to the GitHub Container Registry for each tagged release. To build the image yourself instead of pulling it:

docker build -t parcelwatch .
docker run -d --name parcelwatch -p 8080:8080 -v parcelwatch-data:/data parcelwatch

Configuration

Parcelwatch is configured entirely through environment variables.

Variable Default Description
PARCELWATCH_ADDR :8080 Address the HTTP server listens on.
PARCELWATCH_DB parcelwatch.db Path to the SQLite database file.
PARCELWATCH_URL http://localhost:8080 Public base URL, used in notification links and cookie security.
PARCELWATCH_POLL_INTERVAL 15m How often the poller refreshes active packages.
PARCELWATCH_NTFY_URL https://ntfy.sh Base URL of the ntfy server used for push notifications.
PARCELWATCH_SECRET_KEY (empty) Passphrase used to derive the encryption key. See below.
PARCELWATCH_KEY_FILE <db path>.key Path to the random key file used when no passphrase is set.

When PARCELWATCH_URL starts with https://, session and CSRF cookies are marked Secure automatically.

Accounts and roles

The first account to register becomes the administrator. After that, open registration is controlled by the admin "registration open" toggle. When it is off, no new accounts can be created through the registration page.

Administrators get an extra admin area for configuring the instance and managing users.

Using Parcelwatch

  1. Sign in and open the dashboard.
  2. Pick a carrier, paste the tracking number, and give it an optional label.
  3. Parcelwatch checks the package on a schedule and updates its status. The dashboard refreshes on its own, and you get a notification whenever the status changes.

Open any package to see its full event history and, when available, the delivery photo and route map. Remove a package from the dashboard at any time.

Carrier setup

Carrier credentials are entered by an administrator in the admin area and are stored encrypted. Each carrier needs either an API key (DHL, PostNL) or an OAuth2 client ID and secret (USPS, UPS, FedEx, Royal Mail). A carrier stays inactive until it has valid credentials and is enabled, and the tracking registry reloads whenever those credentials change.

Notifications

Each user chooses how they want to be alerted:

  • ntfy: set a topic (and optional access token) to receive push notifications.
  • Email: set an address to receive updates over the admin-configured SMTP server.

A test button on the settings page confirms the channels are wired up correctly. SMTP host, port, credentials, and the from address are configured once by an administrator and apply to all email notifications.

Map and route history

Route history and the package map are opt-in, both globally (by an admin) and per user. Map tiles default to OpenStreetMap. Geocoding is disabled until an administrator selects a provider (nominatim or photon) and supplies its base URL on purpose, so the instance never reaches a third party without being told to.

Delivery photos

For carriers that expose proof-of-delivery images, Parcelwatch can capture and store the photo with the package. This is a separate per-user toggle, and stored photos are encrypted at rest.

Security

  • Passwords are hashed with Argon2id.
  • Sensitive data (carrier credentials, tracking numbers, route history, delivery photos, and the SMTP password) is encrypted at rest with AES-256-GCM. A keyed blind index keeps encrypted columns searchable and unique without exposing their contents.
  • The encryption key comes from PARCELWATCH_SECRET_KEY (hashed to a 32-byte key) when set. Otherwise a random key is generated on first run and saved to the key file with 0600 permissions.
  • Login attempts are rate limited (5 per 15 minutes).
  • Responses include a content security policy, common security headers, and a request body size limit. Front-end assets are served locally rather than from a CDN.

Keep the key file (or passphrase) safe and backed up. Without it, encrypted data cannot be recovered.

To report a vulnerability, see the security policy.

Running in production

Parcelwatch serves plain HTTP and does not terminate TLS itself. For anything beyond local use, run it behind a reverse proxy that handles HTTPS, such as Caddy, nginx, or Traefik.

  • Set PARCELWATCH_URL to your public https:// address so session and CSRF cookies are marked Secure.
  • Forward traffic to the address from PARCELWATCH_ADDR.
  • Use /healthz for health and uptime checks. It returns ok with a 200 status.

A minimal Caddy config:

parcels.example.com {
    reverse_proxy localhost:8080
}

Login rate limiting keys on the connecting address. Behind a reverse proxy every request appears to come from the proxy, so the limit applies across all clients together. Add per-IP limits at the proxy if you need stricter control.

Updates

Parcelwatch can check GitHub for new releases and surface the release notes in the admin area. Automatic checks are off by default and only run when an administrator opts in.

Set the build version at compile time with ldflags:

go build -ldflags "-X github.com/thatdeercodes/parcelwatch/internal/version.Version=v1.2.3" -o parcelwatch ./cmd/parcelwatch

Backups

All state lives in the SQLite database file. On first run Parcelwatch creates the database (for example parcelwatch.db), its key file (parcelwatch.db.key), and the SQLite working files (parcelwatch.db-wal and parcelwatch.db-shm).

Back up the database and its key file together. Without the key file (or the PARCELWATCH_SECRET_KEY passphrase) the encrypted data cannot be recovered. The .db-wal and .db-shm files do not need to be copied while the server is stopped cleanly.

Development

Run the test suite:

go test ./...

Run a development instance against a throwaway database:

PARCELWATCH_ADDR=":8099" PARCELWATCH_DB="dev.db" go run ./cmd/parcelwatch

Built with

  • Go with the standard library net/http router.
  • modernc.org/sqlite, a pure-Go SQLite driver, so builds need no cgo.
  • htmx for the live dashboard updates, served locally.
  • Leaflet for the package map, served locally.
  • golang.org/x/crypto for Argon2id password hashing.

Contributing

Contributions are welcome.

  • Format code with gofmt and keep go vet ./... and go test ./... clean.
  • Start every new .go file with the SPDX header // SPDX-License-Identifier: AGPL-3.0-or-later.
  • By contributing you agree to license your work under the AGPL-3.0.

These same checks run in CI on every push and pull request.

License

Copyright (C) 2026 thatdeercodes.

Parcelwatch is free software released under the GNU Affero General Public License version 3. You can redistribute it and modify it under those terms, and if you run a modified version as a network service, you must make your source available to its users. See the LICENSE file for the full text.

About

Self-hosted, privacy-first package & shipment tracker for USPS, UPS, FedEx, DHL, Royal Mail & PostNL. ntfy/email alerts, route maps, delivery photos. Single Go binary, runs in Docker.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors