# IGNY8 Plugin Distribution System

**Created:** January 9, 2026  
**Version:** 1.0  
**Status:** ✅ Phase 1 Implemented  
**Scope:** WordPress, Shopify, Custom Site Integration Plugins

---

## Overview

This document outlines the architecture for distributing and auto-updating IGNY8 integration plugins across multiple platforms (WordPress, Shopify, Custom Sites).

---

## 1. Directory Structure

### 1.1 - Server Location

```
/data/app/igny8/
├── backend/
│   └── igny8_core/
│       └── plugins/                    # NEW: Plugin management
│           ├── __init__.py
│           ├── models.py               # Plugin version tracking
│           ├── views.py                # Download & update API
│           ├── serializers.py
│           ├── urls.py
│           └── utils.py                # ZIP compression, versioning
├── plugins/                            # NEW: Plugin source & distributions
│   ├── wordpress/
│   │   ├── source/                     # Development source files
│   │   │   └── igny8-wp-bridge/
│   │   │       ├── igny8-wp-bridge.php
│   │   │       ├── includes/
│   │   │       ├── assets/
│   │   │       └── readme.txt
│   │   └── dist/                       # Compiled/zipped releases
│   │       ├── igny8-wp-bridge-v1.0.0.zip
│   │       ├── igny8-wp-bridge-v1.0.1.zip
│   │       └── igny8-wp-bridge-latest.zip  # Symlink to latest
│   ├── shopify/                        # FUTURE
│   │   ├── source/
│   │   └── dist/
│   └── custom-site/                    # FUTURE
│       ├── source/
│       └── dist/
└── docs/
    └── plans/
        └── PLUGIN-DISTRIBUTION-SYSTEM.md
```

### 1.2 - Why This Structure?

| Location | Purpose |
|----------|---------|
| `/plugins/` | Separate from app code, easy to manage |
| `/plugins/{platform}/source/` | Development files, version controlled |
| `/plugins/{platform}/dist/` | Ready-to-download ZIP files |
| `/backend/igny8_core/plugins/` | Django app for API & version management |

---

## 2. Database Models

### 2.1 - Plugin Model

```python
# backend/igny8_core/plugins/models.py

class Plugin(models.Model):
    """Represents a plugin type (WordPress, Shopify, etc.)"""
    
    PLATFORM_CHOICES = [
        ('wordpress', 'WordPress'),
        ('shopify', 'Shopify'),
        ('custom', 'Custom Site'),
    ]
    
    name = models.CharField(max_length=100)  # "IGNY8 WP Bridge"
    slug = models.SlugField(unique=True)      # "igny8-wp-bridge"
    platform = models.CharField(max_length=20, choices=PLATFORM_CHOICES)
    description = models.TextField()
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        db_table = 'plugins'


class PluginVersion(models.Model):
    """Tracks each version of a plugin"""
    
    STATUS_CHOICES = [
        ('draft', 'Draft'),           # In development
        ('testing', 'Testing'),       # Internal testing
        ('staged', 'Staged'),         # Ready, not yet pushed
        ('released', 'Released'),     # Available for download
        ('update_ready', 'Update Ready'),  # Push to installed sites
        ('deprecated', 'Deprecated'), # Old version
    ]
    
    plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE, related_name='versions')
    version = models.CharField(max_length=20)  # "1.0.0", "1.0.1"
    version_code = models.IntegerField()       # 100, 101 (for comparison)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
    
    # File info
    file_path = models.CharField(max_length=500)  # Relative path in dist/
    file_size = models.IntegerField(default=0)    # Bytes
    checksum = models.CharField(max_length=64)    # SHA256 for integrity
    
    # Release info
    changelog = models.TextField(blank=True)
    min_api_version = models.CharField(max_length=20, default='1.0')
    
    # Timestamps
    created_at = models.DateTimeField(auto_now_add=True)
    released_at = models.DateTimeField(null=True, blank=True)
    
    # Auto-update control
    force_update = models.BooleanField(default=False)  # Critical security fix
    
    class Meta:
        db_table = 'plugin_versions'
        unique_together = ['plugin', 'version']
        ordering = ['-version_code']


class PluginInstallation(models.Model):
    """Tracks where plugins are installed (per site)"""
    
    site = models.ForeignKey('Site', on_delete=models.CASCADE, related_name='plugin_installations')
    plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE)
    current_version = models.ForeignKey(PluginVersion, on_delete=models.SET_NULL, null=True)
    
    # Installation status
    is_active = models.BooleanField(default=True)
    last_health_check = models.DateTimeField(null=True)
    
    # Update tracking
    pending_update = models.ForeignKey(
        PluginVersion, 
        on_delete=models.SET_NULL, 
        null=True, 
        related_name='pending_installations'
    )
    update_notified_at = models.DateTimeField(null=True)
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        db_table = 'plugin_installations'
        unique_together = ['site', 'plugin']
```

---

## 3. API Endpoints

### 3.1 - Public Endpoints (No Auth Required)

```python
# Download latest plugin (for initial installation)
GET /api/plugins/{slug}/download/
# Returns: ZIP file download

# Check for updates (called by installed plugins)
GET /api/plugins/{slug}/check-update/?current_version=1.0.0
# Returns: { "update_available": true, "latest_version": "1.0.1", "download_url": "...", "changelog": "..." }
```

### 3.2 - Authenticated Endpoints

```python
# Get download URL for user (tracks downloads)
GET /api/plugins/{slug}/download-url/
# Returns: { "url": "...", "expires_at": "..." }

# Report installation (called after WP plugin is activated)
POST /api/plugins/{slug}/register-installation/
# Body: { "site_id": 123, "version": "1.0.0" }

# Update installation status (health check)
POST /api/plugins/{slug}/health-check/
# Body: { "site_id": 123, "version": "1.0.0", "status": "active" }
```

### 3.3 - Admin Endpoints

```python
# List all plugin versions
GET /api/admin/plugins/

# Create new version
POST /api/admin/plugins/{slug}/versions/
# Body: { "version": "1.0.1", "changelog": "...", "status": "draft" }

# Upload plugin ZIP
POST /api/admin/plugins/{slug}/versions/{version}/upload/
# Body: multipart/form-data with ZIP file

# Release version (make available for download)
POST /api/admin/plugins/{slug}/versions/{version}/release/

# Push update to installed sites
POST /api/admin/plugins/{slug}/versions/{version}/push-update/
# This sets status to 'update_ready' and notifies all installations
```

---

## 4. WordPress Plugin Update Mechanism

### 4.1 - How WordPress Auto-Updates Work

WordPress checks for plugin updates by calling a filter. Our plugin hooks into this:

```php
// In igny8-wp-bridge.php

class IGNY8_WP_Bridge {
    
    private $plugin_slug = 'igny8-wp-bridge';
    private $version = '1.0.0';
    private $api_url = 'https://app.igny8.com/api/plugins/';
    
    public function __construct() {
        // Check for updates
        add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']);
        add_filter('plugins_api', [$this, 'plugin_info'], 10, 3);
    }
    
    public function check_for_updates($transient) {
        if (empty($transient->checked)) {
            return $transient;
        }
        
        // Call IGNY8 API to check for updates
        $response = wp_remote_get($this->api_url . $this->plugin_slug . '/check-update/', [
            'timeout' => 10,
            'headers' => [
                'X-IGNY8-Site-ID' => get_option('igny8_site_id'),
                'X-IGNY8-API-Key' => get_option('igny8_api_key'),
            ],
            'body' => [
                'current_version' => $this->version,
            ]
        ]);
        
        if (is_wp_error($response)) {
            return $transient;
        }
        
        $data = json_decode(wp_remote_retrieve_body($response), true);
        
        if (!empty($data['update_available'])) {
            $plugin_file = $this->plugin_slug . '/' . $this->plugin_slug . '.php';
            
            $transient->response[$plugin_file] = (object) [
                'slug' => $this->plugin_slug,
                'new_version' => $data['latest_version'],
                'package' => $data['download_url'],
                'url' => $data['info_url'] ?? '',
            ];
        }
        
        return $transient;
    }
    
    public function plugin_info($result, $action, $args) {
        if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
            return $result;
        }
        
        // Fetch plugin info from IGNY8 API
        $response = wp_remote_get($this->api_url . $this->plugin_slug . '/info/');
        
        if (is_wp_error($response)) {
            return $result;
        }
        
        $data = json_decode(wp_remote_retrieve_body($response), true);
        
        return (object) [
            'name' => $data['name'],
            'slug' => $this->plugin_slug,
            'version' => $data['version'],
            'author' => 'IGNY8',
            'homepage' => 'https://igny8.com',
            'sections' => [
                'description' => $data['description'],
                'changelog' => $data['changelog'],
            ],
            'download_link' => $data['download_url'],
        ];
    }
}
```

### 4.2 - Update Flow

```
┌─────────────────────────────────────────────────────────────────────┐
│                     Plugin Update Flow                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. Admin marks version as "Update Ready" in IGNY8 Dashboard        │
│                           │                                          │
│                           ▼                                          │
│  2. IGNY8 API returns update_available=true for check-update calls  │
│                           │                                          │
│                           ▼                                          │
│  3. WordPress cron checks for updates (or user clicks "Check Now")  │
│                           │                                          │
│                           ▼                                          │
│  4. WP Plugin calls IGNY8 API → Gets new version info               │
│                           │                                          │
│                           ▼                                          │
│  5. WordPress shows "Update Available" in Plugins page              │
│                           │                                          │
│                           ▼                                          │
│  6. User clicks Update (or auto-update if enabled)                  │
│                           │                                          │
│                           ▼                                          │
│  7. WordPress downloads ZIP from IGNY8 and installs                 │
│                           │                                          │
│                           ▼                                          │
│  8. Plugin activation hook reports new version to IGNY8             │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 5. Initial Download (ZIP Generation)

### 5.1 - Build Script

```bash
#!/bin/bash
# scripts/build-wp-plugin.sh

VERSION=$1
PLUGIN_NAME="igny8-wp-bridge"
SOURCE_DIR="/data/app/igny8/plugins/wordpress/source/${PLUGIN_NAME}"
DIST_DIR="/data/app/igny8/plugins/wordpress/dist"

if [ -z "$VERSION" ]; then
    echo "Usage: ./build-wp-plugin.sh <version>"
    exit 1
fi

# Create dist directory if not exists
mkdir -p "$DIST_DIR"

# Create temp directory for packaging
TEMP_DIR=$(mktemp -d)
PACKAGE_DIR="${TEMP_DIR}/${PLUGIN_NAME}"

# Copy source files
cp -r "$SOURCE_DIR" "$PACKAGE_DIR"

# Update version in main plugin file
sed -i "s/Version: .*/Version: ${VERSION}/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php"
sed -i "s/\$version = '.*'/\$version = '${VERSION}'/" "${PACKAGE_DIR}/${PLUGIN_NAME}.php"

# Create ZIP
cd "$TEMP_DIR"
zip -r "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" "$PLUGIN_NAME"

# Update latest symlink
ln -sf "${PLUGIN_NAME}-v${VERSION}.zip" "${DIST_DIR}/${PLUGIN_NAME}-latest.zip"

# Calculate checksum
sha256sum "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip" > "${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256"

# Cleanup
rm -rf "$TEMP_DIR"

echo "Built: ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.zip"
echo "Checksum: $(cat ${DIST_DIR}/${PLUGIN_NAME}-v${VERSION}.sha256)"
```

### 5.2 - Django Management Command

```python
# backend/igny8_core/plugins/management/commands/build_plugin.py

from django.core.management.base import BaseCommand
import subprocess
import hashlib
from plugins.models import Plugin, PluginVersion

class Command(BaseCommand):
    help = 'Build and register a new plugin version'
    
    def add_arguments(self, parser):
        parser.add_argument('--plugin', type=str, required=True)
        parser.add_argument('--version', type=str, required=True)
        parser.add_argument('--changelog', type=str, default='')
        parser.add_argument('--release', action='store_true')
    
    def handle(self, *args, **options):
        plugin = Plugin.objects.get(slug=options['plugin'])
        version = options['version']
        
        # Run build script
        result = subprocess.run(
            ['./scripts/build-wp-plugin.sh', version],
            capture_output=True,
            text=True
        )
        
        if result.returncode != 0:
            self.stderr.write(f"Build failed: {result.stderr}")
            return
        
        # Calculate version code (1.0.1 -> 101)
        parts = version.split('.')
        version_code = int(parts[0]) * 10000 + int(parts[1]) * 100 + int(parts[2])
        
        # Get file info
        file_path = f"{plugin.slug}-v{version}.zip"
        full_path = f"/data/app/igny8/plugins/wordpress/dist/{file_path}"
        
        with open(full_path, 'rb') as f:
            checksum = hashlib.sha256(f.read()).hexdigest()
        
        import os
        file_size = os.path.getsize(full_path)
        
        # Create version record
        plugin_version, created = PluginVersion.objects.update_or_create(
            plugin=plugin,
            version=version,
            defaults={
                'version_code': version_code,
                'file_path': file_path,
                'file_size': file_size,
                'checksum': checksum,
                'changelog': options['changelog'],
                'status': 'released' if options['release'] else 'draft',
            }
        )
        
        action = 'Created' if created else 'Updated'
        self.stdout.write(f"{action} version {version} for {plugin.name}")
```

---

## 6. Frontend Integration

### 6.1 - Download Button Component

```tsx
// frontend/src/components/sites/PluginDownloadSection.tsx

import { Button } from '@/components/ui';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/services/api';

interface PluginInfo {
  name: string;
  slug: string;
  version: string;
  download_url: string;
  file_size: number;
  platform: string;
}

export function PluginDownloadSection({ platform = 'wordpress' }: { platform: string }) {
  const { data: plugin, isLoading } = useQuery<PluginInfo>({
    queryKey: ['plugin', platform],
    queryFn: () => api.get(`/plugins/${platform}/latest/`).then(r => r.data),
  });

  const handleDownload = async () => {
    if (!plugin) return;
    
    // Track download
    await api.post(`/plugins/${plugin.slug}/track-download/`);
    
    // Trigger download
    window.location.href = plugin.download_url;
  };

  if (isLoading) return <div>Loading...</div>;
  if (!plugin) return null;

  return (
    <div className="bg-white rounded-xl border border-gray-200 p-6">
      <div className="flex items-start gap-4">
        <div className="p-3 bg-green-50 rounded-lg">
          <DownloadIcon className="w-6 h-6 text-green-600" />
        </div>
        <div className="flex-1">
          <h3 className="font-semibold text-gray-900">{plugin.name}</h3>
          <p className="text-sm text-gray-500 mt-1">
            Download and install the plugin on your {platform === 'wordpress' ? 'WordPress' : platform} site
          </p>
          <div className="flex items-center gap-4 mt-2 text-xs text-gray-400">
            <span>Version {plugin.version}</span>
            <span>{formatFileSize(plugin.file_size)}</span>
          </div>
        </div>
        <Button onClick={handleDownload} variant="outline">
          <DownloadIcon className="w-4 h-4 mr-2" />
          Download Plugin
        </Button>
      </div>
    </div>
  );
}
```

### 6.2 - Admin Plugin Management Page (Future)

```
/admin/plugins                    # List all plugins
/admin/plugins/{slug}             # Plugin details + versions
/admin/plugins/{slug}/versions    # Version history
/admin/plugins/{slug}/upload      # Upload new version
```

---

## 7. Security Considerations

### 7.1 - Download Security

| Measure | Implementation |
|---------|----------------|
| Signed URLs | Time-limited download URLs (15 min expiry) |
| Checksum | SHA256 verification in WP plugin |
| API Key Required | Downloads require valid site API key |
| Rate Limiting | Max 10 downloads/hour per site |

### 7.2 - Update Security

| Measure | Implementation |
|---------|----------------|
| HTTPS Only | All API calls over TLS |
| Version Signing | Checksum comparison before install |
| Rollback | Keep last 3 versions for emergency rollback |
| Staged Rollout | Option to release to % of installs first |

---

## 8. Implementation Phases

### Phase 1: Basic Infrastructure (Week 1) ✅ COMPLETED
- [x] Create `/plugins/` directory structure
- [x] Create Django `plugins` app with models
- [x] Migrate existing WP plugin to `/plugins/wordpress/source/`
- [x] Create build script for ZIP generation
- [x] Implement download API endpoint

### Phase 2: Frontend Integration (Week 1) ✅ COMPLETED
- [x] Update Site Settings > Integrations download button
- [x] Create plugin info API endpoint
- [x] Add version display to download section

### Phase 3: Update System (Week 2) ✅ COMPLETED
- [x] Implement check-update API
- [x] Add update hooks to WP plugin
- [x] Create PluginInstallation tracking
- [x] Build admin version management UI

### Phase 4: Advanced Features (Week 3)
- [ ] Signed download URLs
- [ ] Version rollback support
- [ ] Update notifications in IGNY8 dashboard
- [ ] Staged rollout percentage

### Phase 5: Additional Platforms (Future)
- [ ] Shopify plugin structure
- [ ] Custom site SDK/package
- [ ] Platform-specific update mechanisms

---

## 9. File Serving Configuration

### 9.1 - Nginx Configuration

```nginx
# Add to nginx config for igny8

# Serve plugin downloads
location /plugins/download/ {
    alias /data/app/igny8/plugins/;
    
    # Only allow ZIP files
    location ~ \.zip$ {
        add_header Content-Disposition 'attachment';
        add_header X-Content-Type-Options 'nosniff';
    }
    
    # Deny access to source files
    location ~ /source/ {
        deny all;
    }
}
```

### 9.2 - Django URL (Proxied Downloads)

```python
# Better approach: Django handles download with auth

# backend/igny8_core/plugins/views.py

from django.http import FileResponse
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny

@api_view(['GET'])
@permission_classes([AllowAny])
def download_plugin(request, slug):
    plugin = get_object_or_404(Plugin, slug=slug, is_active=True)
    latest = plugin.versions.filter(status__in=['released', 'update_ready']).first()
    
    if not latest:
        return Response({'error': 'No version available'}, status=404)
    
    file_path = f'/data/app/igny8/plugins/{plugin.platform}/dist/{latest.file_path}'
    
    response = FileResponse(
        open(file_path, 'rb'),
        content_type='application/zip'
    )
    response['Content-Disposition'] = f'attachment; filename="{latest.file_path}"'
    return response
```

---

## 10. Monitoring & Analytics

### 10.1 - Metrics to Track

| Metric | Purpose |
|--------|---------|
| Downloads per version | Adoption rate |
| Active installations | Current reach |
| Update success rate | Quality indicator |
| Version distribution | Upgrade urgency |
| Health check failures | Support needs |

### 10.2 - Admin Dashboard Widgets

- **Plugin Downloads** - Daily/weekly download counts
- **Version Distribution** - Pie chart of installed versions
- **Update Adoption** - Time to 80% adoption after release
- **Installation Health** - Sites with outdated/failing plugins

---

## Quick Reference

### Commands

```bash
# Build new version
python manage.py build_plugin --plugin=igny8-wp-bridge --version=1.0.1 --changelog="Bug fixes"

# Release version (make downloadable)
python manage.py release_plugin --plugin=igny8-wp-bridge --version=1.0.1

# Push update to sites
python manage.py push_plugin_update --plugin=igny8-wp-bridge --version=1.0.1
```

### API Quick Reference

```
GET  /api/plugins/{slug}/download/           # Download latest ZIP
GET  /api/plugins/{slug}/check-update/       # Check for updates
GET  /api/plugins/{slug}/info/               # Plugin metadata
POST /api/plugins/{slug}/register/           # Register installation
POST /api/plugins/{slug}/health-check/       # Report status
```

---

**Document Owner:** IGNY8 Team  
**Next Review:** After Phase 1 implementation
