# Ankylix License Analytics Server

This document provides example server implementations for tracking license activations.

## Overview

The Ankylix app sends **non-blocking analytics pings** when a license is activated. This allows you to:
- 📊 Track how many times each license key has been activated
- 🖥️ See which devices are using each license
- 📈 Monitor activation patterns
- 🚨 Detect potential key sharing/abuse

**Important**: The app works offline and never blocks activation if the server is unreachable.

---

## Data Sent to Server

Each activation sends a JSON payload:

```json
{
  "order_id": "ord_01h1234567890abcdef",
  "hardware_id": "a3f5e7b9c1d2e4f6a8b0c2d4e6f8a0b2",
  "app_version": "1.0.0",
  "os_version": "Version 14.5 (Build 23F79)",
  "timestamp": "2026-04-12T14:30:00Z",
  "event": "license_activated"
}
```

### Field Descriptions:

- **order_id**: Paddle order ID (e.g., `ord_abc123xyz`)
- **hardware_id**: SHA256 hash of Mac serial number (privacy-friendly, unique per Mac)
- **app_version**: App version number
- **os_version**: macOS version
- **timestamp**: ISO 8601 timestamp
- **event**: Always "license_activated"

---

## Option 1: Node.js + Express + SQLite

Simple self-hosted solution:

### 1. Install Dependencies

```bash
npm install express sqlite3 body-parser
```

### 2. Server Code (`server.js`)

```javascript
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// Initialize SQLite database
const db = new sqlite3.Database('./activations.db');

// Create activations table
db.run(`
  CREATE TABLE IF NOT EXISTS activations (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    order_id TEXT NOT NULL,
    hardware_id TEXT NOT NULL,
    app_version TEXT,
    os_version TEXT,
    timestamp TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(order_id, hardware_id)
  )
`);

// Analytics endpoint
app.post('/v1/activations', (req, res) => {
  const { order_id, hardware_id, app_version, os_version, timestamp } = req.body;
  
  // Validate required fields
  if (!order_id || !hardware_id) {
    return res.status(400).json({ error: 'Missing required fields' });
  }
  
  // Insert or update activation
  db.run(
    `INSERT INTO activations (order_id, hardware_id, app_version, os_version, timestamp)
     VALUES (?, ?, ?, ?, ?)
     ON CONFLICT(order_id, hardware_id) DO UPDATE SET
       app_version = excluded.app_version,
       os_version = excluded.os_version,
       timestamp = excluded.timestamp`,
    [order_id, hardware_id, app_version, os_version, timestamp],
    (err) => {
      if (err) {
        console.error('Database error:', err);
        return res.status(500).json({ error: 'Database error' });
      }
      
      console.log(`✅ Activation recorded: ${order_id} on ${hardware_id.substring(0, 8)}...`);
      res.json({ success: true, message: 'Activation recorded' });
    }
  );
});

// Dashboard endpoint - Get activation counts
app.get('/v1/dashboard/activations', (req, res) => {
  db.all(
    `SELECT 
       order_id,
       COUNT(*) as device_count,
       MAX(timestamp) as last_activation
     FROM activations
     GROUP BY order_id
     ORDER BY device_count DESC`,
    (err, rows) => {
      if (err) {
        return res.status(500).json({ error: 'Database error' });
      }
      res.json(rows);
    }
  );
});

// Get details for specific order
app.get('/v1/dashboard/order/:orderId', (req, res) => {
  db.all(
    `SELECT * FROM activations WHERE order_id = ? ORDER BY created_at DESC`,
    [req.params.orderId],
    (err, rows) => {
      if (err) {
        return res.status(500).json({ error: 'Database error' });
      }
      res.json(rows);
    }
  );
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`🚀 Analytics server running on port ${PORT}`);
});
```

### 3. Run Server

```bash
node server.js
```

### 4. Deploy

Deploy to:
- **Railway.app** - Free tier available
- **Fly.io** - Free tier available
- **DigitalOcean App Platform** - $5/month
- **Your own VPS** - Any price

---

## Option 2: Cloudflare Workers + D1 (Serverless)

Free up to 100k requests/day:

```javascript
export default {
  async fetch(request, env) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    try {
      const data = await request.json();
      const { order_id, hardware_id, app_version, os_version, timestamp } = data;

      // Insert into D1 database
      await env.DB.prepare(
        `INSERT INTO activations (order_id, hardware_id, app_version, os_version, timestamp)
         VALUES (?, ?, ?, ?, ?)
         ON CONFLICT(order_id, hardware_id) DO UPDATE SET
           app_version = excluded.app_version,
           timestamp = excluded.timestamp`
      ).bind(order_id, hardware_id, app_version, os_version, timestamp).run();

      return Response.json({ success: true });
    } catch (err) {
      console.error(err);
      return Response.json({ error: 'Server error' }, { status: 500 });
    }
  }
}
```

---

## Option 3: Supabase (Backend-as-a-Service)

Free tier: 500MB database, 50k monthly active users

### 1. Create Supabase Project

Visit [supabase.com](https://supabase.com)

### 2. Create Table

```sql
CREATE TABLE activations (
  id BIGSERIAL PRIMARY KEY,
  order_id TEXT NOT NULL,
  hardware_id TEXT NOT NULL,
  app_version TEXT,
  os_version TEXT,
  timestamp TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(order_id, hardware_id)
);

-- Create index for faster queries
CREATE INDEX idx_order_id ON activations(order_id);
```

### 3. Use Supabase REST API

Update the URL in `TrialManager.swift`:

```swift
guard let url = URL(string: "https://YOUR_PROJECT.supabase.co/rest/v1/activations") else {
    return
}

request.setValue("YOUR_ANON_KEY", forHTTPHeaderField: "apikey")
request.setValue("Bearer YOUR_ANON_KEY", forHTTPHeaderField: "Authorization")
```

---

## Querying Your Data

### See all activations for an order:

```sql
SELECT * FROM activations 
WHERE order_id = 'ord_abc123xyz' 
ORDER BY created_at DESC;
```

### Find orders with multiple activations:

```sql
SELECT 
  order_id, 
  COUNT(*) as device_count,
  MAX(timestamp) as last_used
FROM activations 
GROUP BY order_id 
HAVING COUNT(*) > 2
ORDER BY device_count DESC;
```

### Find potentially shared keys (10+ devices):

```sql
SELECT order_id, COUNT(*) as devices
FROM activations
GROUP BY order_id
HAVING COUNT(*) >= 10;
```

### Monthly activation trend:

```sql
SELECT 
  DATE_TRUNC('month', created_at) as month,
  COUNT(*) as new_activations
FROM activations
GROUP BY month
ORDER BY month DESC;
```

---

## Dashboard Examples

### Simple HTML Dashboard

```html
<!DOCTYPE html>
<html>
<head>
  <title>Ankylix License Dashboard</title>
  <style>
    body { font-family: system-ui; padding: 20px; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    th { background: #f0f0f0; }
    .warning { color: orange; }
    .danger { color: red; }
  </style>
</head>
<body>
  <h1>License Activations</h1>
  <table id="activations">
    <thead>
      <tr>
        <th>Order ID</th>
        <th>Devices</th>
        <th>Last Activation</th>
        <th>Status</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <script>
    fetch('/v1/dashboard/activations')
      .then(r => r.json())
      .then(data => {
        const tbody = document.querySelector('#activations tbody');
        data.forEach(row => {
          const status = row.device_count > 5 ? 'danger' : 
                        row.device_count > 2 ? 'warning' : '';
          tbody.innerHTML += `
            <tr>
              <td><a href="/v1/dashboard/order/${row.order_id}">${row.order_id}</a></td>
              <td class="${status}">${row.device_count}</td>
              <td>${new Date(row.last_activation).toLocaleString()}</td>
              <td class="${status}">${
                row.device_count > 5 ? '🚨 Possible sharing' :
                row.device_count > 2 ? '⚠️ Multiple devices' :
                '✅ Normal'
              }</td>
            </tr>
          `;
        });
      });
  </script>
</body>
</html>
```

---

## Privacy Considerations

### What We Store:
- ✅ **Hardware ID**: SHA256 hash of serial number (not reversible)
- ✅ **Order ID**: Already public (user has it)
- ✅ **App/OS version**: Public information
- ✅ **Timestamp**: When activated

### What We DON'T Store:
- ❌ User email addresses
- ❌ Real serial numbers
- ❌ IP addresses (unless your hosting adds them)
- ❌ User names
- ❌ Location data

### GDPR Compliance:
- Hardware IDs are hashed → not personally identifiable
- Users can request deletion of their order_id data
- No tracking cookies needed
- Anonymous by design

---

## Testing Locally

### 1. Use ngrok to expose local server:

```bash
ngrok http 3000
```

### 2. Update TrialManager.swift:

```swift
guard let url = URL(string: "https://abc123.ngrok.io/v1/activations") else {
    return
}
```

### 3. Activate a test license in debug mode

Watch your server logs for:
```
✅ Activation recorded: ord_test123 on a3f5e7b9...
```

---

## Next Steps

1. ✅ **Choose a hosting option** (I recommend Railway.app or Supabase for ease)
2. ✅ **Deploy the server code**
3. ✅ **Update the URL** in `TrialManager.swift`
4. ✅ **Test with debug license**
5. ✅ **Monitor your dashboard**

## Security Note

This is **analytics-only** - it doesn't validate licenses or block users. The app works completely offline. You're just collecting data to:

- See usage patterns
- Detect abuse
- Make informed decisions about DRM

**Don't add hard limits** until you have data showing it's actually a problem!

---

## Questions?

The analytics are now built into your app. When you're ready to deploy the server:

1. Copy one of the server examples above
2. Deploy it somewhere
3. Update the URL in TrialManager.swift
4. Watch the data come in!

Let me know if you need help with deployment! 🚀
