Anyone who's tried to buy popular Ubiquiti networking gear knows the pain: products like the Dream Machine Pro Max, USW-Pro-Max switches, and Protect cameras sell out within minutes of restocking. The Ubiquiti store at ui.com does have a "notify me" button, but it's a first-come-first-served queue — by the time your notification arrives, hundreds or thousands of other buyers have already been alerted ahead of you. There's no public API, no way to know your position in the queue, and no guarantee you'll actually be able to buy anything by the time you click through.
We built UIPing (opens in new tab) to solve this — a monitoring service that watches ui.com product pages at configurable intervals and pushes instant notifications the moment anything changes: stock status, pricing, new product listings, or page content.
Architecture Overview
UIPing is a full-stack SaaS with a Python backend for monitoring and task orchestration, and a React frontend for the dashboard. The core architecture:
- Backend: FastAPI with SQLAlchemy ORM, serving a REST API
- Task queue: Celery with Redis as broker, handling all check scheduling and notification dispatch
- Database: PostgreSQL for persistent storage (users, monitors, snapshots, changes, subscriptions)
- Cache/state: Redis for rate limiting, in-flight tracking, and Celery result backend
- Frontend: React 19 + TypeScript + Tailwind CSS 4, built with Vite
- Payments: Stripe for subscription billing with webhook-driven lifecycle management
- Visual diffing: Playwright for headless screenshots, Pillow + pixelmatch for pixel-level comparison
- VPN rotation: WireGuard with 96 exit nodes across the Americas and Europe
The Change Detection Engine
The core of UIPing is the change detection pipeline. Instead of checking each monitor independently (which would mean duplicate HTTP requests for users watching the same product page), we consolidated checks at the URL level.
URL-Based Check Consolidation
Every 10 seconds, a Celery beat task dispatches due checks. It groups all active monitors by URL, determines the effective check interval (the fastest paying subscriber's interval wins), and issues a single HTTP request per URL. That one response is then processed against every monitor watching that page.
This consolidation dramatically reduces outbound request volume. If 50 users are all watching the Dream Machine Pro Max page, we make one request instead of 50 — and each user gets their result as if they had a dedicated check.
HTML Text Diffing
When a page is fetched, we extract the visible text content using BeautifulSoup. Each monitor can optionally specify a CSS selector to target a specific section of the page (like a product card or price element) and ignore selectors to exclude noisy elements (navigation, footers, breadcrumbs).
The extracted text is hashed with SHA-256. If the hash changes from the previous snapshot, we generate a diff to show exactly what changed. We built a custom diff renderer that produces highlighted HTML output showing additions and removals.
Noise Suppression
Early in development, we discovered that raw page monitoring generates a lot of false positives. CDN edge servers return slightly different content, breadcrumb text flickers, and minor DOM reordering triggers diffs. We added two layers of noise suppression:
- Minimum diff threshold: Changes below 1% text difference are suppressed as rendering noise
- Flip-flop detection: If the new content hash matches any of the last 10 snapshots, the change is suppressed as CDN oscillation — this catches cases where different edge servers return different stock states on alternating requests
Stock-Specific Pattern Detection
Beyond generic text diffing, we added pattern recognition for Ubiquiti-specific stock signals. The system detects transitions like "Sold Out" to "Add to Cart", price changes (regex-matched dollar amounts), and new product appearances. When a stock-relevant pattern is identified, the diff summary is prefixed with a human-readable description like "Back in stock!" or "Price changed: $699 → $649".
Visual Screenshot Comparison
Some page changes aren't visible in the text — layout shifts, image swaps, badge additions, or JavaScript-rendered content that doesn't appear in the initial HTML. For these cases, UIPing offers visual monitoring.
We use Playwright (Chromium headless) to take full-page screenshots at configurable intervals. Screenshots are compared using pixelmatch, a pixel-level image comparison library. If more than 0.5% of pixels differ between the current and previous screenshot, a visual change is recorded along with a diff image highlighting the changed regions.
Like HTML checks, visual checks are consolidated by URL. One screenshot serves all monitors watching the same page (grouped by CSS selector, since monitors targeting different page regions need different captures).
VPN Rotation System
Monitoring a single website at high frequency from a fixed IP raises an obvious challenge: rate limiting and IP blocking. To handle this, we built a VPN rotation system using WireGuard.
The system maintains 96 WireGuard configurations across exit nodes in the US, Canada, Mexico, the Caribbean, Central America, South America, and Europe. When the change detector receives a 403 response, it automatically triggers a VPN rotation — acquiring a Redis lock (to prevent stampede when multiple monitors all hit 403 simultaneously), bringing down the current tunnel, bringing up a randomly selected new one, and retrying the request.
The rotation script supports both sequential cycling and random selection. A Redis-based lock ensures only one rotation happens at a time, with a 60-second timeout and 30-second blocking wait. After rotation, a 2-second stabilization pause allows the new tunnel to establish before retrying.
Notification System
Detecting a change is only half the job — the other half is getting that information to the user as fast as possible. UIPing supports six notification channels:
- Email: Available on all plans including free. Branded HTML emails with diff summaries
- ntfy push: Self-hosted ntfy server for instant mobile push notifications on iOS and Android
- Home Assistant: Direct integration with HA notification services for smart home automation
- Discord webhooks: Rich embeds with change type, percentage, and direct links
- Slack webhooks: Block Kit formatted messages with monitor links
- Custom webhooks: JSON payloads with optional HMAC-SHA256 signing for verification
All notification dispatch is async via Celery tasks routed to a dedicated notifications queue. Webhooks include a retry mechanism with consecutive failure tracking — after repeated failures, the webhook is automatically disabled to prevent resource waste.
Webhook Payload Design
Custom webhooks receive a JSON payload containing the event type, monitor details (ID, name, URL), and change details (type, summary, diff percentage, timestamp). For custom webhooks with a configured secret, the payload body is signed with HMAC-SHA256 and the signature is included in an X-UIPing-Signature header for server-side verification.
Discord and Slack get specially formatted payloads — Discord uses rich embeds with colored sidebars, while Slack uses Block Kit with mrkdwn formatting.
Subscription Billing with Stripe
UIPing uses a four-tier pricing model managed entirely through Stripe:
- Free: 1 monitor, 30-minute check interval, email notifications only
- Starter ($7/mo): 5 monitors, 10-minute intervals, all notification channels
- Pro ($25/mo): 25 monitors, 5-minute intervals, visual diffing, priority support
- Enterprise ($100/mo): 100 monitors, 1-second intervals, dedicated support
All plans support annual billing at a discount. The billing system handles the full subscription lifecycle through Stripe webhooks: checkout completion, subscription updates, cancellations, payment failures, and even chargebacks.
Graceful Downgrade
When a paid subscription ends, the system doesn't delete anything. It deactivates all monitors, re-enables only the most recently created one (matching the free tier's 1-monitor limit), creates a new free-tier subscription, and sends the user an email listing exactly which monitors were paused. Monitors and their change history are preserved indefinitely, so upgrading later restores everything.
The React Frontend
The frontend is a single-page application built with React 19, TypeScript, React Router, and Tailwind CSS 4 (via Vite plugin). Key libraries include:
- TanStack React Query: Server state management with automatic refetching, optimistic updates, and cache invalidation
- Zustand: Lightweight client state for auth context and UI preferences
- Lucide React: Icon library for consistent UI elements
- diff2html: Rendering HTML diffs in the change detail view
- Axios: HTTP client with interceptors for JWT refresh token rotation
SEO Product Pages
Since UIPing targets users searching for specific Ubiquiti products, we pre-render individual product pages with SEO-optimized content. Each page targets keywords like "UDM Pro Max in stock alert" and includes product descriptions, monitoring tips, FAQ sections with schema markup, and related product suggestions. The product catalog currently covers gateways, switches, wireless access points, cameras, and access control products.
Celery Task Architecture
The task system is the backbone of UIPing. Celery handles everything async:
- checks queue: HTML content checks (URL-consolidated batches)
- visual queue: Playwright screenshot captures and pixel comparisons
- notifications queue: Email, ntfy, Home Assistant, webhook, and Discord/Slack delivery
- maintenance queue: Snapshot cleanup, Stripe subscription sync, notification log pruning, onboarding emails
- beat queue: Dispatcher tasks that run on schedule and fan out work to other queues
The beat schedule dispatches HTML checks every 10 seconds, visual checks every 60 seconds, snapshot cleanup hourly, Stripe sync every 6 hours, and a monthly digest email. All tasks use acks_late for reliability — if a worker crashes mid-task, the message returns to the queue for retry.
Monthly Digest
UIPing sends a monthly digest email to all active users summarizing the hottest Ubiquiti products (most monitored and most changes detected), personal stats (how many changes their monitors caught), and platform-wide stats (total changes detected, total alerts sent). The digest uses branded HTML email templates with CAN-SPAM compliant unsubscribe links.
Security and Authentication
Authentication uses JWT with separate access and refresh tokens. Access tokens expire in 30 minutes, refresh tokens in 7 days. The system includes:
- bcrypt password hashing via Passlib
- Email verification with 24-hour token expiry
- Password reset flow with 1-hour tokens
- One-click email unsubscribe tokens (365-day expiry, CAN-SPAM compliant)
- CORS restricted to production origin (plus localhost in debug mode)
- Trusted host middleware rejecting requests for unrecognized hostnames
- Global exception handler that never leaks internal error details
- Stripe webhook signature verification for all billing events
- Redis-based idempotency on Stripe events (24-hour dedup window)
Database Models
The PostgreSQL schema includes 8 models:
- User: Account with notification preferences, Stripe customer ID, Home Assistant and ntfy configuration, onboarding state
- Subscription: Stripe subscription binding with tier, status, check intervals, and monitor limits
- Monitor: A watched URL with check type (HTML, visual, or both), CSS selectors, ignore selectors, custom headers, browser rendering flag, and error tracking
- Snapshot: Point-in-time capture of a page — content hash, extracted text, raw HTML, screenshot path, response time
- Change: A detected difference between two snapshots — diff HTML, summary, percentage, screenshot diff path
- Notification: In-app notification (change, error, billing, system types) with read tracking
- NotificationLog: Delivery log for external notifications (email, webhook, ntfy, etc.) with status and error tracking
- Webhook: User-configured webhook endpoint with type (Discord, Slack, custom), secret for HMAC signing, monitor filtering, and event filtering
Migrations are managed with Alembic. All UUID primary keys use PostgreSQL's native UUID type. JSONB columns store flexible data like monitor headers. ARRAY columns store ignore selectors and webhook monitor/event filters.
Infrastructure
UIPing runs on a self-hosted Proxmox VM with Nginx as the reverse proxy, Uvicorn serving the FastAPI application, and systemd managing all services. The frontend is built with Vite and served as static files with a custom prerender script that generates HTML for SEO product pages.
The stack includes a self-hosted ntfy server for push notifications, a local Redis instance for Celery and caching, and PostgreSQL for persistent storage. WireGuard tunnels terminate directly on the VM for VPN rotation.
Key Takeaways
- Consolidate at the data layer: URL-based check batching reduced our outbound request volume by an order of magnitude while giving every user their expected check interval
- Noise suppression is essential: Without minimum diff thresholds and flip-flop detection, users would drown in false positives from CDN edge variation
- VPN rotation needs coordination: A Redis lock prevents multiple workers from all trying to rotate the VPN simultaneously when a wave of 403s hits
- Graceful degradation over hard cutoffs: Preserving monitors and history on downgrade means upgrading is frictionless — nothing is lost
- Self-hosted push notifications: Running our own ntfy server keeps notification latency under control and eliminates dependency on third-party push services
UIPing is live at uiping.com (opens in new tab) with a free tier that lets you monitor one Ubiquiti product page with email alerts — no credit card required.