PakTrack is a self-hosted, single-user package tracking dashboard for monitoring parcels from Belgian and Benelux-region carriers. Paste a tracking code, and PakTrack auto-detects the carrier, polls for status updates, and sends push notifications via Pushover when the status changes.
- Multi-Carrier Support: Track parcels from Bpost, PostNL, GLS, and Colis Prive
- Real Carrier APIs: All carriers use live tracking APIs (no API keys required)
- Bpost: Public
track.bpost.cloudJSON API - PostNL: Public
jouw.postnl.nltracking API (auto-derives country from postal code) - GLS: Public
api.gls-group.euREST API - Colis Prive: HTML scraping with cheerio
- Bpost: Public
- Auto-Detection: Automatically detects carrier from tracking code format
- Manual Override: Select any carrier manually when auto-detection doesn't match
- Background Polling: Configurable interval polling with random drift (anti-bot)
- Push Notifications: Get notified via Pushover on status changes
- Smart Polling:
- Polls active parcels only
- Continues polling delivered parcels for 24 hours
- 15-minute cooldown between notifications per parcel
- Carrier circuit breaker (backs off after 10 consecutive failures)
- Per-parcel jitter (0-60s) to space out requests
- Per-cycle drift (+-10 minutes) to avoid fixed intervals
- Timeline View: Visual timeline of all tracking events
- Archive Page: Move parcels out of the active dashboard without deleting their history
- Auto-Archive Rules: Automatically archive delivered parcels after 7, 14, or 30 days
- Delivery Calendar: Group active and archived deliveries by estimated arrival date or delivered date
- Carrier Diagnostics: Inspect poller health, carrier backoff state, and recent carrier errors from Settings
- Notification Rules: Globally choose which update types are worth sending to Pushover
- Analytics & History: Review lifecycle totals, carrier mix, delivery timing, and archived parcel history from one page
- PWA Support: Install on iOS/Android home screen with offline support
- Dark Mode: System-aware dark mode with manual override
- Per-Parcel Notifications: Toggle notifications per parcel
- Copy Tracking: One-click copy tracking code to clipboard
- Labels: Add custom labels to identify packages
- Archive Management: Restore archived parcels back to the active dashboard at any time
- Calendar View: Browse estimated arrivals, deliveries, and parcels without a current ETA in one timeline-focused page
- Diagnostics Panel: See whether background polling is healthy for each carrier without leaving the app
- Notification Categories: Enable or suppress routine transit, delivery-day, delivered, or problem alerts globally
- History Dashboard: See current delivery throughput, archived totals, and recent lifecycle activity without leaving the app
- Dedicated Archive View: Keep using
/archiveas a focused restore view even though History is now the main lifecycle dashboard
| Layer | Technology |
|---|---|
| Frontend | React 19 + Vite + Tailwind CSS |
| Backend | Node.js 22 + Express |
| Database | SQLite (better-sqlite3) |
| ORM | Drizzle ORM |
| Notifications | Pushover API |
| HTML Scraping | cheerio |
| PWA | vite-plugin-pwa |
| Container | Docker |
# Clone the repository
git clone https://github.com/Du7chManiac/PakTrack.git
cd paktrack
# Copy environment file and configure
cp env.example .env
# Edit .env with your Pushover credentials (optional)
# Build and start
docker compose up -d --buildNote: The included
docker-compose.ymlis designed for use behind a reverse proxy (Traefik, Nginx Proxy Manager, etc.) and does not expose ports. For local or standalone use, add a port mapping:services: paktrack: ports: - '3000:3000'
- Node.js 22+
- npm
# Install all dependencies (root + workspaces)
npm installStart the development servers:
npm run devThis will start:
- Backend API on http://localhost:3000
- Frontend dev server on http://localhost:5173
# Build frontend
npm run build
# Run production server
cd backend && node src/index.js| Variable | Default | Description |
|---|---|---|
PORT |
3000 | Server listen port |
DATABASE_PATH |
./data/paktrack.db locally, /data/paktrack.db in production containers when /data exists |
Path to SQLite database |
PUSHOVER_USER_KEY |
- | Pushover user key (optional) |
PUSHOVER_API_TOKEN |
- | Pushover API token (optional) |
PUSHOVER_DEVICE |
- | Target device name for notifications (optional, sends to all devices if omitted) |
POLL_INTERVAL_MINUTES |
30 | Polling interval (15, 30, 60, 120, 240) |
LOG_LEVEL |
info | Logging level (debug, info, warn, error) |
NODE_ENV |
development | Environment mode |
See env.example for a ready-to-use template.
- Create a Pushover account at https://pushover.net
- Note your User Key from the dashboard
- Create a new Application to get an API Token
- Enter both in Settings > Pushover Notifications
- Optionally, enter a Device Name to send notifications only to a specific device (e.g.
iphone,myandroid). Leave blank to send to all your registered devices. - Click "Test Notification" to verify
You can also configure credentials and the target device via environment variables:
PUSHOVER_USER_KEY=your_user_key
PUSHOVER_API_TOKEN=your_api_token
PUSHOVER_DEVICE=iphone # optional — omit to send to all devicesNote: Environment variables take priority over values stored in the Settings UI.
PakTrack automatically detects carriers based on tracking code format:
| Carrier | Format | Example |
|---|---|---|
| Bpost (International) | 2 letters + 9 digits + BE | CE123456789BE |
| Bpost (Domestic) | 24-26 digits starting with 32/30 | 323298761234987648753589 |
| Bpost (JJBEA) | Starts with JJBEA | JJBEA1234567891012345 |
| PostNL (3S) | Starts with 3S or 2S | 3SABCD1234567 |
| PostNL (International) | 2 letters + 9 digits + NL | RR123456789NL |
| GLS (Short) | 8 alphanumeric | ZWKV4CV6 |
| GLS (Numeric) | 11-14 digits | 47150051801147 |
| Colis Prive | Fallback / Manual | Any 10-15 characters |
When adding a parcel, all 4 carriers are shown. Auto-detected carriers appear first with confidence badges, and remaining carriers are available below for manual override. This is useful for cross-border parcels (e.g., a PostNL-format code tracked by Bpost).
All carrier integrations use public endpoints — no API keys or business accounts required.
| Carrier | Endpoint | Auth | Notes |
|---|---|---|---|
| Bpost | track.bpost.cloud/track/items |
None | Postal code improves results |
| PostNL | jouw.postnl.nl/track-and-trace/api/trackAndTrace |
None | Auto-derives country (BE/NL) from postal code |
| GLS | api.gls-group.eu/.../rstt001 |
None | Public REST API |
| Colis Prive | colisprive.com/.../detailColis.aspx |
None | HTML scraping, most fragile |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/parcels |
List parcels. No query returns all parcels, ?archived=false returns active parcels, ?archived=true returns archived parcels, and ?calendar=true&from=<iso>&to=<iso> returns dated calendar entries within a range |
| POST | /api/parcels |
Add new parcel. Body: { trackingCode, postalCode, label?, carrier? } |
| GET | /api/parcels/:id |
Get parcel with events |
| PATCH | /api/parcels/:id |
Update label, notify, isArchived, archiveReason, or archiveSource |
| DELETE | /api/parcels/:id |
Remove parcel and events |
| POST | /api/parcels/:id/refresh |
Force immediate poll |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/detect |
Detect carrier. Body: { trackingCode } |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/settings |
Get all settings (masks Pushover token) |
| PATCH | /api/settings |
Update settings including Pushover credentials, poll interval, auto-archive rules, and notification_rules |
| POST | /api/settings/test-notification |
Send test notification |
Archive-related parcel responses now include isArchived, archivedAt, archiveReason, archiveSource, deliveredAt, and lastStatusChangeAt so the frontend can show archive lifecycle details.
The delivery calendar uses estimatedDelivery for parcels that are still moving, and deliveredAt for parcels that have already been delivered. Parcels without a usable date are omitted from the dated buckets and shown in the calendar's No ETA section.
Analytics and history use parcel lifecycle timestamps already stored by PakTrack: createdAt, deliveredAt, archivedAt, and lastStatusChangeAt. Archived parcels are included in history metrics by default so delivery history stays visible even after parcels leave the active dashboard.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check with version and uptime |
| GET | /api/health/poller |
Poller diagnostics including carrier failures, carrier backoff, and recent poll timing |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/analytics/history?days=30 |
Aggregate lifecycle history for the requested window, including current snapshots, carrier rollups, and daily created/delivered/archived counts |
Carrier diagnostics live in Settings and expose PakTrack's current poller state: active parcel counts, the configured poll interval, carrier degradation or backoff, and the last successful or failed poll timestamps. Diagnostics are kept in memory and reset when the app restarts.
notification_rules is a global settings object with four booleans: in_transit_updates, delivery_day_updates, delivered_updates, and problem_updates. These rules apply only to parcels with per-parcel notifications enabled, and the existing cooldown still suppresses rapid repeat notifications.
The main navigation now points archived workflows to History, which combines lifecycle analytics with restore controls for archived parcels. The existing /archive route still remains available as a dedicated archived-only view.
The included docker-compose.yml works with reverse proxy setups (Traefik, Nginx Proxy Manager, Coolify, etc.) where the proxy handles port mapping and SSL.
For standalone Docker deployment with direct port access:
docker run -d \
--name paktrack \
--restart unless-stopped \
-p 3000:3000 \
-v /path/to/data:/data \
-e DATABASE_PATH=/data/paktrack.db \
-e NODE_ENV=production \
paktrackPakTrack works with Docker-based deployment platforms:
- Point the platform at this repo or use the included
docker-compose.yml - Configure a bind mount for
/datato persist the SQLite database - No port mappings needed if using a reverse proxy (Traefik, etc.)
- SSL via your platform's certificate management
server {
listen 80;
server_name paktrack.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}sudo certbot --nginx -d paktrack.yourdomain.comThe SQLite database is stored at the path specified by DATABASE_PATH (default: ./data/paktrack.db locally, /data/paktrack.db in production containers). To backup:
# Copy the database file
cp /path/to/data/paktrack.db /backup/paktrack-$(date +%Y%m%d).db# Run all tests
npm test
# Run backend tests only
npm run test:backend
# Run frontend tests only
npm run test:frontendpaktrack/
├── frontend/ # React frontend
│ ├── public/ # Static assets & PWA icons
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # Page components
│ │ ├── hooks/ # Custom hooks
│ │ └── lib/ # Utilities
│ └── test/ # Frontend tests
├── backend/ # Node.js backend
│ ├── src/
│ │ ├── carriers/ # Carrier adapters & detection
│ │ ├── db/ # Database schema & setup
│ │ ├── routes/ # API routes
│ │ ├── services/ # Poller & Pushover
│ │ └── utils/ # Logger, settings helper
│ └── test/ # Backend tests
├── Dockerfile # Multi-stage Docker build
├── docker-compose.yml # Docker Compose config
├── env.example # Environment variable template
└── package.json # Root monorepo config
- Verify Pushover credentials in Settings
- Check that parcel has notifications enabled
- Verify the Device Name in Settings matches an actual registered device (or leave blank for all devices)
- Test notification from Settings page
- Check server logs for errors
- Verify the matching notification rule is enabled in Settings > Pushover Notifications
- Remember that fast back-to-back updates can still be skipped by the cooldown window
- Check History first, or open
/archivefor the dedicated archived-only view - Delivered parcels can be auto-archived from Settings after 7, 14, or 30 days
- Unarchive a parcel to return it to the active dashboard and resume active polling
- Many carriers do not provide an estimated delivery date until a parcel is close to arrival
- Parcels without an ETA are grouped under
No ETAinstead of a calendar day - Delivered parcels remain visible through their delivered date, even after they are archived
- Backoff pauses automatic polling for that carrier after repeated failures and usually points to a carrier API or scraping issue
- Diagnostics reset on restart, so a freshly restarted app may show healthy counters even if a carrier was failing earlier
- Manual refresh can still be useful while a carrier is marked degraded, but repeated failures usually mean the carrier endpoint changed
- History includes archived parcels by default so past deliveries remain visible after they leave active tracking
- Delivery timing is calculated from when a parcel was added to PakTrack until it reached a delivered state
- Unarchiving a parcel removes it from the current archived snapshot because PakTrack does not keep a separate archive event log yet
- Carrier websites/APIs may have changed (check logs)
- Circuit breaker may be active (waits 1 hour after 10 failures)
- Try manual refresh from parcel detail page
- Colis Prive is most fragile (HTML scraping) — may break if page structure changes
- Ensure only one instance is running
- Check file permissions on data directory
- Restart the container/service
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.
- Originally built as a personal project
- "Pak" is Dutch for package