# Deployment Guide — AnimFX Store

Complete deployment instructions for **shared hosting**, **VPS/cloud**, **Docker**, and **CI/CD**.

---

## 📋 Pre-Deployment Checklist

| Item | Required | Notes |
|------|----------|-------|
| PHP 8.1+ | ✅ | `intl`, `mbstring`, `curl`, `openssl`, `pdo_mysql`/`pdo_sqlite` |
| Composer 2+ | ✅ | For `composer install --no-dev` |
| MySQL/MariaDB 10.3+ | ✅ | **Required for production** (SQLite not suitable) |
| SSL Certificate | ✅ | Let's Encrypt or paid |
| Domain DNS configured | ✅ | A/AAAA/CNAME pointing to server |
| Webhook URLs accessible | ✅ | Public HTTPS, no auth, no redirects |

---

## 🌐 Option 1: Shared Hosting (cPanel / Plesk / DirectAdmin)

### 1. Prepare Locally
```bash
# In project root
composer install --no-dev --optimize-autoloader

# Remove dev files
rm -rf tests/ _*.php writable/cache/* writable/logs/* writable/session/*

# Verify production .env exists
cat .env | grep -E 'CI_ENVIRONMENT|app.baseURL|database.default'
```

### 2. Database: SQLite → MySQL Migration

**Shared hosting does NOT support SQLite reliably** (file locking, permissions). You must migrate to MySQL.

#### A. Create MySQL Database (cPanel)
1. **MySQL Databases** → Create database: `youruser_svga`
2. Create user: `youruser_svga` + strong password
3. Add user to database → **All Privileges**

#### B. Export Data from Local SQLite
```bash
# Dump data only (no schema)
sqlite3 writable/database/svga_store.db .dump --data-only > sqlite_data.sql

# Fix for MySQL:
# - Remove: BEGIN TRANSACTION; / COMMIT;
# - Replace: "sqlite_sequence" references (not needed, MySQL uses AUTO_INCREMENT)
# - Ensure: INSERT statements use backticks for identifiers
```

#### C. Import to MySQL
**Via phpMyAdmin (cPanel):**
1. Select database → **Import** → choose `sqlite_data.sql` → **Go**

**Via SSH (if available):**
```bash
mysql -u youruser_svga -p youruser_svga < sqlite_data.sql
```

#### D. Or: Fresh Schema + Seed (Simpler)
If you don't need old orders/users:
```bash
# On server (via SSH or cron PHP script)
php spark migrate --all
php spark db:seed MainSeeder
```

### 3. Upload Files
**File Manager / FTP / SFTP:**
```
public_html/                    ← Document root for main domain
├── .env
├── spark
├── composer.json
├── app/
├── vendor/
├── writable/
└── public/                     ← Contents go HERE if docroot = public_html
    ├── index.php
    ├── .htaccess
    └── assets/
```

**Critical:** Document Root must point to `public/` folder.

**cPanel → Domains → Addon/Subdomain → Document Root:**
```
/home/youruser/public_html/public
```

**If you CANNOT change document root** (some cheap hosting), use `.htaccess` in `public_html/`:
```apache
# public_html/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/public/
RewriteRule ^(.*)$ /public/$1 [L,QSA]
```

### 4. Set Permissions
```bash
# Via File Manager or SSH
chmod 755 writable/
chmod 755 writable/cache/ writable/logs/ writable/session/ writable/uploads/
chmod 644 .env
```

### 5. Configure .env (Production)
```dotenv
CI_ENVIRONMENT = production
app.baseURL = 'https://yourdomain.com/'

# Database (from cPanel MySQL Databases)
database.default.hostname = localhost
database.default.database = youruser_svga
database.default.username = youruser_svga
database.default.password = 'your_strong_password'
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306
database.default.charset = utf8mb4
database.default.DBCollat = utf8mb4_unicode_ci

# Payment webhooks must use HTTPS
# Update in Paddle/Binance/PayPal dashboards after SSL is active
```

### 6. Run Migrations (if fresh DB)
**Option A: SSH (best)**
```bash
cd /home/youruser/public_html
php spark migrate --all
php spark db:seed MainSeeder
```

**Option B: PHP Script (no SSH)**
Create `public/run-migrate.php`:
```php
<?php
// DELETE AFTER USE!
require __DIR__ . '/../vendor/autoload.php';
$app = require __DIR__ . '/../app/Config/Boot/production.php';
$app->run(); // Runs migrations if configured in App\Config\Events
```
Access `https://yourdomain.com/run-migrate.php` → **delete immediately**.

**Option C: cPanel Cron Job (one-time)**
```bash
# Cron: * * * * * /usr/local/bin/php /home/youruser/public_html/spark migrate --all 2>&1
# Remove after success
```

### 7. SSL & HTTPS
**cPanel → SSL/TLS → Let's Encrypt** → Issue for domain → **Force HTTPS Redirect: On**

### 8. Update Webhook URLs
| Provider | New URL |
|----------|---------|
| Paddle | `https://yourdomain.com/payment/webhook/paddle` |
| Binance | `https://yourdomain.com/payment/webhook/binance` |
| PayPal | `https://yourdomain.com/payment/webhook/paypal` |

---

## 🖥 Option 2: VPS / Cloud Server (Ubuntu 22.04/24.04)

### 1. Server Setup
```bash
# As root
apt update && apt upgrade -y
apt install -y nginx php8.3-fpm php8.3-cli php8.3-mysql php8.3-xml php8.3-mbstring \
    php8.3-curl php8.3-zip php8.3-gd php8.3-intl php8.3-bcmath php8.3-redis \
    mariadb-server composer git unzip certbot python3-certbot-nginx

# PHP-FPM tuning
sed -i 's/^;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/' /etc/php/8.3/fpm/php.ini
sed -i 's/^upload_max_filesize = .*/upload_max_filesize = 10M/' /etc/php/8.3/fpm/php.ini
sed -i 's/^post_max_size = .*/post_max_size = 12M/' /etc/php/8.3/fpm/php.ini
systemctl restart php8.3-fpm
```

### 2. Database
```bash
mysql_secure_installation
mysql -u root -p -e "
CREATE DATABASE svga_store CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'svga_user'@'localhost' IDENTIFIED BY 'StrongPasswordHere';
GRANT ALL PRIVILEGES ON svga_store.* TO 'svga_user'@'localhost';
FLUSH PRIVILEGES;"
```

### 3. Deploy Application
```bash
mkdir -p /var/www
cd /var/www
git clone https://github.com/yourusername/svga-store.git
cd svga-store

composer install --no-dev --optimize-autoloader
cp .env.example .env
# Edit .env with production values (see below)
```

### 4. Production .env
```dotenv
CI_ENVIRONMENT = production
app.baseURL = 'https://yourdomain.com/'
app.forceGlobalSecureRequests = true

database.default.hostname = localhost
database.default.database = svga_store
database.default.username = svga_user
database.default.password = 'StrongPasswordHere'
database.default.DBDriver = MySQLi
database.default.DBPrefix =
database.default.port = 3306
database.default.charset = utf8mb4
database.default.DBCollat = utf8mb4_unicode_ci

# Store settings
store_name = 'AnimFX Store'
store_email = 'admin@yourdomain.com'
store_currency = 'USD'
store_currency_symbol = '$'

# ... payment gateway credentials ...
```

### 5. Migrate & Seed
```bash
php spark migrate --all
php spark db:seed MainSeeder
```

### 6. Permissions
```bash
chown -R www-data:www-data /var/www/svga-store/writable
chmod -R 755 /var/www/svga-store/writable
chmod 640 /var/www/svga-store/.env
chown root:www-data /var/www/svga-store/.env
```

### 7. Nginx Config (`/etc/nginx/sites-available/svga-store`)
```nginx
server {
    listen 80;
    listen [::]:80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/svga-store/public;
    index index.php;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
    }

    # Deny access to sensitive files
    location ~* \.(env|git|gitignore|htaccess|lock|sql|sqlite|db)$ {
        deny all;
        return 404;
    }

    location ~ /\.ht {
        deny all;
    }

    # Cache static assets
    location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Uploads - no PHP execution
    location /uploads/ {
        location ~ \.php$ { deny all; }
    }
}
```

Enable:
```bash
ln -s /etc/nginx/sites-available/svga-store /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
```

### 8. SSL (Let's Encrypt)
```bash
certbot --nginx -d yourdomain.com -d www.yourdomain.com
# Choose: 2 (Redirect HTTP to HTTPS)
# Auto-renewal: systemctl status certbot.timer (enabled by default)
```

### 9. Systemd Services (Optional but Recommended)

**Scheduler (`/etc/systemd/system/ci4-scheduler.service`):**
```ini
[Unit]
Description=CodeIgniter 4 Scheduler
After=network.target

[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/svga-store
ExecStart=/usr/bin/php spark schedule:run
```

**Timer (`/etc/systemd/system/ci4-scheduler.timer`):**
```ini
[Unit]
Description=Run CI4 Scheduler every minute

[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true

[Install]
WantedBy=timers.target
```

Enable:
```bash
systemctl daemon-reload
systemctl enable --now ci4-scheduler.timer
```

---

## 🐳 Option 3: Docker (Multi-Stage Build)

### `Dockerfile`
```dockerfile
# ---- Build Stage ----
FROM composer:2.7 AS builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress

# ---- PHP-FPM Stage ----
FROM php:8.3-fpm-alpine

# Install extensions
RUN apk add --no-cache \
    linux-headers \
    $PHPIZE_DEPS \
    libzip-dev \
    libpng-dev \
    libjpeg-turbo-dev \
    freetype-dev \
    icu-dev \
    oniguruma-dev \
    postgresql-dev \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j$(nproc) \
    pdo_mysql \
    pdo_sqlite \
    zip \
    gd \
    intl \
    mbstring \
    opcache \
    bcmath \
    && pecl install redis \
    && docker-php-ext-enable redis \
    && apk del $PHPIZE_DEPS

# Opcache config
RUN echo "opcache.enable=1\nopcache.memory_consumption=128\nopcache.interned_strings_buffer=8\nopcache.max_accelerated_files=10000\nopcache.revalidate_freq=60\nopcache.fast_shutdown=1" > /usr/local/etc/php/conf.d/opcache.ini

WORKDIR /var/www/html

# Copy vendor from builder
COPY --from=builder /app/vendor ./vendor

# Copy application
COPY . .

# Permissions
RUN chown -R www-data:www-data writable \
    && chmod -R 755 writable

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/ || exit 1

EXPOSE 9000
CMD ["php-fpm"]
```

### `docker-compose.yml`
```yaml
version: '3.8'

services:
  app:
    build: .
    container_name: svga-app
    restart: unless-stopped
    volumes:
      - ./writable:/var/www/html/writable
      - ./public:/var/www/html/public:ro
    environment:
      - CI_ENVIRONMENT=production
    depends_on:
      - db
    networks:
      - svga-net

  web:
    image: nginx:alpine
    container_name: svga-web
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./public:/var/www/html/public:ro
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - app
    networks:
      - svga-net

  db:
    image: mariadb:10.11
    container_name: svga-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: svga_store
      MYSQL_USER: svga_user
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - svga-net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  certbot:
    image: certbot/certbot
    container_name: svga-certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done'"

volumes:
  db-data:

networks:
  svga-net:
    driver: bridge
```

### `nginx.conf` (for Docker)
```nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/html/public;
    index index.php;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/html/public;
    index index.php;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 300;
    }

    location ~* \.(env|git|sql|sqlite|lock)$ { deny all; }
}
```

### Deploy
```bash
# 1. Create .env with DB_PASSWORD, DB_ROOT_PASSWORD
# 2. Start
docker compose up -d --build

# 3. Run migrations (first time)
docker compose exec app php spark migrate --all
docker compose exec app php spark db:seed MainSeeder

# 4. Get SSL
docker compose run --rm certbot certonly --webroot -w /var/www/certbot -d yourdomain.com -d www.yourdomain.com
docker compose restart web
```

---

## 🔄 Option 4: CI/CD — GitHub Actions

### `.github/workflows/deploy.yml`
```yaml
name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  SSH_HOST: ${{ secrets.SSH_HOST }}
  SSH_USER: ${{ secrets.SSH_USER }}
  SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
  APP_DIR: /var/www/svga-store

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, xml, curl, zip, intl, bcmath, sqlite3
          tools: composer:v2
      - run: composer install --no-dev --optimize-autoloader
      - run: php -l $(find app -name "*.php")
      - run: php spark migrate --all --force 2>&1 || true  # Test migration syntax
      - run: php tests/manual/_paddle_e2e_test.php  # Requires dev server, skip in CI if no DB

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.9
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Deploy via rsync
        run: |
          rsync -avz --delete \
            --exclude '.git' \
            --exclude '.github' \
            --exclude 'tests' \
            --exclude '_*' \
            --exclude 'writable/cache/*' \
            --exclude 'writable/logs/*' \
            --exclude 'writable/session/*' \
            --exclude '.env' \
            ./ ${{ env.SSH_USER }}@${{ env.SSH_HOST }}:${{ env.APP_DIR }}/

      - name: Composer install on server
        run: |
          ssh ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "
            cd ${{ env.APP_DIR }} &&
            composer install --no-dev --optimize-autoloader --no-interaction
          "

      - name: Run migrations
        run: |
          ssh ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "
            cd ${{ env.APP_DIR }} &&
            php spark migrate --all --force
          "

      - name: Clear cache
        run: |
          ssh ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "
            cd ${{ env.APP_DIR }} &&
            php spark cache:clear &&
            php spark optimize
          "

      - name: Reload PHP-FPM
        run: |
          ssh ${{ env.SSH_USER }}@${{ env.SSH_HOST }} "
            sudo systemctl reload php8.3-fpm
          "
```

### Required GitHub Secrets
| Secret | Value |
|--------|-------|
| `SSH_HOST` | `your-server-ip` |
| `SSH_USER` | `deploy` (or `www-data` with sudo) |
| `SSH_PRIVATE_KEY` | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
| `DB_PASSWORD` | For Docker Compose |

---

## 📦 Backup Strategy

### Database Backup (Daily Cron)
```bash
# /etc/cron.daily/backup-svga
#!/bin/bash
DATE=$(date +%F_%H-%M)
mysqldump -u svga_user -p'StrongPasswordHere' svga_store | gzip > /backups/svga_${DATE}.sql.gz
find /backups -name 'svga_*.sql.gz' -mtime +30 -delete
```
```bash
chmod +x /etc/cron.daily/backup-svga
```

### Uploads Backup (Weekly)
```bash
# /etc/cron.weekly/backup-uploads
#!/bin/bash
DATE=$(date +%F)
tar -czf /backups/uploads_${DATE}.tar.gz /var/www/svga-store/writable/uploads/
find /backups -name 'uploads_*.tar.gz' -mtime +60 -delete
```

### Offsite (Rclone to S3/Wasabi/Backblaze)
```bash
rclone copy /backups remote:svga-backups/
```

---

## 🔍 Post-Deploy Verification

```bash
# 1. Health check
curl -I https://yourdomain.com/
# Expect: 200 OK, security headers present

# 2. Payment webhooks accessible
curl -X POST https://yourdomain.com/payment/webhook/paddle \
  -H "Content-Type: application/json" \
  -d '{"event_type":"test"}'
# Expect: 400 (invalid signature) — NOT 404/500

# 3. Admin login
# Visit https://yourdomain.com/admin → login with seeded admin

# 4. Create test order
# Add to cart → checkout → select Bank Transfer → verify order created

# 5. Check logs
tail -f /var/www/svga-store/writable/logs/log-$(date +%Y-%m-%d).log
```

---

## 🚨 Troubleshooting

| Error | Cause | Fix |
|-------|-------|-----|
| `500 Internal Server Error` | `.env` missing, permissions, PHP error | Check `writable/logs/`, fix `.env`, `chmod 755 writable/` |
| `404 on all routes` | Nginx `try_files` wrong, document root not `public/` | Fix nginx root, ensure `index.php` in public |
| `Database connection failed` | Wrong credentials, MySQL not running | Verify `.env`, `systemctl status mariadb` |
| `Webhook 400/500` | Signature verification failed, missing webhook ID | Check provider dashboard, ensure HTTPS, verify keys |
| `Session not working` | `writable/session/` not writable, cookie domain | `chmod 755 writable/session/`, check `app.sessionCookieDomain` |
| `CSRF mismatch` | HTTPS→HTTP redirect, proxy stripping headers | `app.forceGlobalSecureRequests=true`, proxy `X-Forwarded-Proto` |

---

## 📞 Support Contacts

| Component | Contact |
|-----------|---------|
| Server/Infra | Your DevOps / Hosting Support |
| Paddle | seller-support@paddle.com |
| Binance Pay | merchant@binance.com |
| PayPal | developer@paypal.com |
| Domain/SSL | Your Registrar / Let's Encrypt Community |

---

*Last updated: 2026-09-27*