DEV Community

Milinda Biswas
Milinda Biswas

Posted on • Originally published at avluz.com

Building a Privacy-First Price Alert System: Zero Cookies, Full Trust

Cover Image

"We need to see what products users are tracking."

That's what our analytics team asked for during a product meeting. Seems reasonable, right? Understanding user behavior helps build better products.

But here's what they were really asking: permission to collect data about users' shopping interests, price sensitivities, browsing patterns, and purchasing intentions. In other words, everything a traditional price tracker does without thinking twice.

We said no.

That decision led us down a path that seemed crazy at first: building a price alert system that genuinely doesn't know what users are tracking. At Avluz.com, where we monitor prices across 10,000+ products from Amazon, eBay, and Walmart, we've proven you can build powerful features without sacrificing user privacy.

Here's how we achieved zero cookies, 100% local-first processing, and 12,000 users in our first quarter—all while staying completely GDPR compliant.

Tracking vs Privacy

The Privacy Problem Nobody Talks About

Most price trackers are surveillance systems in disguise. I audited five popular competitors, and here's what I found:

  • Average cookies per site: 47
  • Third-party tracking scripts: 23
  • Data shared with partners: 100% of user behavior
  • User consent: Dark patterns everywhere

Users create accounts, set price alerts, and unknowingly hand over a detailed map of their shopping psychology. This data gets monetized through:

  1. Targeted advertising
  2. Affiliate link manipulation
  3. Data broker sales
  4. "Anonymous" analytics (that aren't really anonymous)

Privacy Comparison Chart

The insight that changed everything: Users don't need us to store their alerts. They just need us to check prices and notify them. Everything else can happen locally.

The Architecture: Zero-Knowledge by Design

Here's what we built:

Architecture Diagram

Core Principles

  1. Local-First Storage: All alert data lives in the user's browser
  2. Client-Side Encryption: Even we can't read user data
  3. Anonymous Identifiers: No linkage between alerts and users
  4. Minimal Backend: Only checks prices, never stores user intent

The flow looks like this:

Privacy Workflow

Implementation: The Code That Makes It Work

1. Client-Side Encryption with Web Crypto API

/**
 * Privacy-preserving encryption using AES-256-GCM
 * Data never leaves the device unencrypted
 */
class PrivacyVault {
    constructor() {
        this.algorithm = 'AES-GCM';
        this.keyLength = 256;
    }

    async generateKey() {
        return await crypto.subtle.generateKey(
            {
                name: this.algorithm,
                length: this.keyLength
            },
            true,
            ['encrypt', 'decrypt']
        );
    }

    async encrypt(data, key) {
        const encoder = new TextEncoder();
        const iv = crypto.getRandomValues(new Uint8Array(12));

        const encrypted = await crypto.subtle.encrypt(
            {
                name: this.algorithm,
                iv: iv
            },
            key,
            encoder.encode(JSON.stringify(data))
        );

        // Return IV + encrypted data
        return {
            iv: Array.from(iv),
            data: Array.from(new Uint8Array(encrypted))
        };
    }

    async decrypt(encryptedPackage, key) {
        const decoder = new TextDecoder();

        const decrypted = await crypto.subtle.decrypt(
            {
                name: this.algorithm,
                iv: new Uint8Array(encryptedPackage.iv)
            },
            key,
            new Uint8Array(encryptedPackage.data)
        );

        return JSON.parse(decoder.decode(decrypted));
    }
}

// Usage
const vault = new PrivacyVault();
const userKey = await vault.generateKey();

// Encrypt alert locally
const alert = {
    productUrl: 'https://amazon.com/product/...',
    targetPrice: 299.99,
    notificationMethod: 'email'
};

const encrypted = await vault.encrypt(alert, userKey);
Enter fullscreen mode Exit fullscreen mode

Encryption Code

Key insight: The encryption key never leaves the browser. We use IndexedDB to store it, secured by the browser's origin policy.

2. Anonymous Alert Registration

/**
 * Type-safe alert storage without user identification
 */
interface AnonymousAlert {
    alertId: string;          // Random UUID, no user link
    productHash: string;      // SHA-256 of product URL
    checkInterval: number;    // How often to check
    notificationToken: string; // Encrypted notification endpoint
}

class AlertManager {
    private db: IDBDatabase;

    async registerAlert(alert: {
        productUrl: string;
        targetPrice: number;
        email: string;
    }): Promise<string> {
        // Generate anonymous ID
        const alertId = crypto.randomUUID();

        // Hash product URL (one-way, can't reverse)
        const productHash = await this.hashProductUrl(alert.productUrl);

        // Encrypt notification details
        const vault = new PrivacyVault();
        const key = await vault.generateKey();
        const encryptedEmail = await vault.encrypt(
            { email: alert.email },
            key
        );

        // Store locally with encryption key
        await this.storeLocally({
            alertId,
            productUrl: alert.productUrl,
            targetPrice: alert.targetPrice,
            encryptedEmail,
            key
        });

        // Register with backend (no user data)
        await fetch('/api/alerts/register', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                alertId,
                productHash,
                checkInterval: 3600 // 1 hour
            })
        });

        return alertId;
    }

    private async hashProductUrl(url: string): Promise<string> {
        const encoder = new TextEncoder();
        const data = encoder.encode(url);
        const hash = await crypto.subtle.digest('SHA-256', data);
        return Array.from(new Uint8Array(hash))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }

    private async storeLocally(data: any): Promise<void> {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction(['alerts'], 'readwrite');
            const store = transaction.objectStore('alerts');
            const request = store.add(data);

            request.onsuccess = () => resolve();
            request.onerror = () => reject(request.error);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Code

What this achieves:

  • Backend only knows a hashed product identifier
  • Backend doesn't know the user's email or target price
  • Backend can't correlate alerts to users
  • User can delete data instantly (it's local)

3. Privacy-Preserving Notifications

/**
 * Notify users without revealing whn��#tHere's what we were dealing with:
eNotifier {
    async sendPriceDropNotification(alertId, newPrice) {
        // Retrieve locally stored data
        const alert = await this.getLocalAlert(alertId);

        // Decrypt notification details
        const vault = new PrivacyVault();
        const decrypted = await vault.decrypt(
            alert.encryptedEmail,
            alert.key
        );

        // Send notification via privacy-respecting service
        // We use a queue that doesn't log recipient details
        await fetch('/api/notifications/queue', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                recipient: decrypted.email,
                template: 'price-drop',
                data: {
                    product: alert.productUrl,
                    newPrice: newPrice,
                    targetPrice: alert.targetPrice
                },
                // Privacy flags
                doNotLog: true,
                doNotTrack: true,
                deleteAfterSend: true
            })
        });
    }

    async getLocalAlert(alertId) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction(['alerts'], 'readonly');
            const store = transaction.objectStore('alerts');
            const request = store.get(alertId);

            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The notification pattern:

  1. Backend detects price drop
  2. Backend sends generic "check alert {alertId}" message
  3. Client retrieves local data
  4. Client decrypts email
  5. Client requests notification (via privacy-preserving queue)
  6. Queue sends email and immediately forgets

GDPR Compliance: Actually Easy When You Don't Store Data

GDPR Compliance

Here's the beautiful thing: most GDPR requirements disappear when you don't collect personal data.

What We Achieved

Right to Access: "We don't have your data. Check your browser's IndexedDB."

Right to Deletion: "Delete your local storage. We already don't have it."

Data Portability: "Export from IndexedDB. Here's the code to do it."

Data Minimization: ✅ We literally can't collect less data

Purpose Limitation: ✅ We only check prices, nothing else

Storage Limitation: ✅ Data lives in user's browser, they control retention

The One GDPR Requirement We Actually Handle

Consent for notifications: We still need explicit consent to send emails. Our implementation:

class ConsentManager {
    async requestNotificationConsent() {
        // Clear, specific language
        const consent = await this.showConsentDialog({
            title: 'Price Drop Notifications',
            message: `
                We'll send you an email when prices drop below your target.

                Privacy guarantee:
                • We don't store your email on our servers
                • We don't track which products you're watching
                • We don't share data with anyone
                • You can stop anytime by clearing your browser data
            `,
            acceptButton: 'Yes, notify me',
            rejectButton: 'No thanks',
            learnMoreLink: '/privacy-policy'
        });

        if (consent.accepted) {
            await this.storeLocalConsent({
                notificationConsent: true,
                timestamp: Date.now(),
                version: '1.0'
            });
        }

        return consent.accepted;
    }

    async storeLocalConsent(consent) {
        localStorage.setItem('privacyConsent', JSON.stringify(consent));
    }

    hasValidConsent() {
        const consent = JSON.parse(localStorage.getItem('privacyConsent'));
        return consent && consent.notificationConsent === true;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Results: Privacy as Competitive Advantage

User Growth

Six months after launch:

Key Metrics

  • 12,000 users in first quarter
  • 0 cookies stored
  • 0 GDPR complaints (there's nothing to complain about)
  • 100% local processing of user data
  • AES-256 encryption standard
  • 47% conversion rate (vs 23% industry average)

Why the high conversion? Trust. When we tell users "we literally can't see what you're tracking," they believe us because it's architecturally impossible.

Avluz Dashboard

This approach now powers the privacy-focused alert system at Avluz.com, where users set price alerts without sacrificing privacy.

What We Learned the Hard Way

1. IndexedDB Has Limits

Browser storage quotas vary. We hit limits at ~1000 alerts per user. Solution:

async function checkStorageQuota() {
    if (navigator.storage && navigator.storage.estimate) {
        const estimate = await navigator.storage.estimate();
        const percentUsed = (estimate.usage / estimate.quota) * 100;

        if (percentUsed > 80) {
            // Warn user to clean up old alerts
            showStorageWarning();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Encryption Is Computationally Expensive

Encrypting/decrypting on every alert check killed performance. Solution: Cache decrypted data in memory with short TTL.

class EncryptionCache {
    constructor(ttl = 300000) { // 5 minutes
        this.cache = new Map();
        this.ttl = ttl;
    }

    async get(alertId, key, decryptFn) {
        const cached = this.cache.get(alertId);
        if (cached && Date.now() - cached.timestamp < this.ttl) {
            return cached.data;
        }

        const decrypted = await decryptFn(key);
        this.cache.set(alertId, {
            data: decrypted,
            timestamp: Date.now()
        });

        return decrypted;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Users Lose Data When Clearing Browser Storage

This is a feature, not a bug. But we needed to educate users:

  • Added export functionality
  • Provided clear warnings before data-clearing actions
  • Built import/export for multi-device sync (still encrypted)

4. Anonymous Analytics Are Still Possible

We track:

  • Total alert count (no user link)
  • Popular product categories (hashed)
  • System performance metrics
  • Error rates (sanitized)

All without identifying individual users.

The Technical Stack

Here's what makes this work:

Frontend:

  • Web Crypto API for encryption
  • IndexedDB for local storage
  • Service Workers for background sync
  • Web Push API for notifications (with consent)

Backend:

  • Minimal Express.js API
  • Redis for price cache (no user data)
  • PostgreSQL for product prices (not alert data)
  • Zero-log notification queue

Infrastructure:

  • All hosted in EU (GDPR compliance)
  • No third-party analytics
  • No CDN tracking
  • Self-hosted fonts and assets

Challenges You'll Face

Multi-Device Sync Without a Server

Users want alerts on phone and desktop. Our solution:

class EncryptedSync {
    async exportAlerts() {
        const alerts = await this.getAllLocalAlerts();
        const vault = new PrivacyVault();

        // Encrypt entire export with user-provided passphrase
        const passphrase = await this.requestPassphrase();
        const key = await this.deriveKeyFromPassphrase(passphrase);
        const encrypted = await vault.encrypt(alerts, key);

        // Generate QR code or download file
        return {
            data: encrypted,
            format: 'encrypted-json',
            instructions: 'Use this file to import on other devices'
        };
    }

    async deriveKeyFromPassphrase(passphrase) {
        const encoder = new TextEncoder();
        const keyMaterial = await crypto.subtle.importKey(
            'raw',
            encoder.encode(passphrase),
            'PBKDF2',
            false,
            ['deriveKey']
        );

        return await crypto.subtle.deriveKey(
            {
                name: 'PBKDF2',
                salt: encoder.encode('avluz-privacy-salt'),
                iterations: 100000,
                hash: 'SHA-256'
            },
            keyMaterial,
            { name: 'AES-GCM', length: 256 },
            true,
            ['encrypt', 'decrypt']
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Browser Compatibility

Web Crypto API support is good but not universal. Fallback:

const hasWebCrypto = window.crypto && window.crypto.subtle;

if (!hasWebCrypto) {
    // Fallback to a privacy-respecting polyfill
    // Or gracefully degrade to non-encrypted storage with clear warning
    showPrivacyWarning('Your browser doesn\'t support encryption. Consider upgrading.');
}
Enter fullscreen mode Exit fullscreen mode

Privacy as Marketing

The unexpected benefit: privacy became our strongest marketing message.

Our landing page headline:

"We don't know what products you're tracking. And we like it that way."

Conversion rate doubled. Why?

  1. Differentiation: Everyone else tracks users
  2. Trust: Actions match words (verifiable in code)
  3. Compliance: No cookie banner needed
  4. European market: GDPR-native attracts EU users

Comparison Table

Resources and Further Reading

Want to go deeper? These resources helped us:

The Future: Privacy-First Everything

This isn't just about price alerts. The patterns we've developed work for:

  • Password managers (already common)
  • Note-taking apps (emerging)
  • Budget trackers (underserved market)
  • Health apps (desperately needed)
  • Any app that stores personal preferences

The technology exists. The user demand is there. What's missing is developers willing to build without data collection.

Try It Yourself

Want to implement something similar? Start here:

  1. Design for local-first from day one - Retrofitting is painful
  2. Use Web Crypto API - Don't roll your own encryption
  3. Think in anonymous identifiers - Unlearn user-centric design
  4. Make privacy visible - Users need to see the difference
  5. Test GDPR compliance early - It's easier than you think

The complete code patterns (sanitized for public use) are in this GitHub repo.

Your Turn: Build Privacy-First

At Avluz.com, we've proven that privacy and functionality aren't opposites. You can build better products by collecting less data.

The question isn't "can we build this without tracking users?" It's "why aren't more people doing this?"

If you're building something similar, I'd love to hear about it. What privacy challenges are you facing? What creative solutions have you found?

See our privacy-first approach in action on the Avluz.com price alerts dashboard.


Questions? Drop them in the comments. I'm happy to discuss technical details, privacy patterns, or GDPR compliance.

Building privacy-first features? Let's share knowledge. The more of us doing this, the better the web becomes for everyone.


This article represents 2 years of building privacy-focused features at Avluz.com. All code examples are from our production system (sanitized for security). The 12,000 user metric is real, and yes—we really don't know what they're tracking.

Top comments (0)