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.
- Why
- Features
- Supported carriers
- Requirements
- Quick start
- Configuration
- Accounts and roles
- Using Parcelwatch
- Carrier setup
- Notifications
- Map and route history
- Delivery photos
- Security
- Running in production
- Updates
- Backups
- Development
- Built with
- Contributing
- License
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.
- 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.
| 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.
- Docker to run the container image.
- Carrier API credentials for whichever carriers you want to track.
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:latestThen 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 parcelwatchParcelwatch 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.
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.
- Sign in and open the dashboard.
- Pick a carrier, paste the tracking number, and give it an optional label.
- 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 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.
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.
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.
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.
- 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 with0600permissions. - 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.
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_URLto your publichttps://address so session and CSRF cookies are markedSecure. - Forward traffic to the address from
PARCELWATCH_ADDR. - Use
/healthzfor health and uptime checks. It returnsokwith 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.
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/parcelwatchAll 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.
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- Go with the standard library
net/httprouter. - 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/cryptofor Argon2id password hashing.
Contributions are welcome.
- Format code with
gofmtand keepgo vet ./...andgo test ./...clean. - Start every new
.gofile 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.
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.