# Deployment Guide — cPanel

This guide installs ICSSPK Events on standard cPanel shared hosting with
**no SSH, no root access and no Composer on the server**. Steps that are
easier with SSH say so, and each has a no-SSH alternative.

> Status: Phase 2 (core infrastructure + public site). There is no admin
> login yet (Phase 3) and no payment integration yet (Phase 8). Cron jobs are
> listed at the end for when those phases land.

---

## 0. Requirements

| Item | Requirement |
|---|---|
| PHP | **8.3 or newer** (tested on 8.4) |
| PHP extensions | `pdo`, `pdo_mysql`, `curl`, `gd`, `fileinfo`, `openssl`, `mbstring`, `json` (`intl` optional) |
| Database | MySQL 8.0.16+ **or** MariaDB 10.6+ (CHECK constraints must be enforced; older versions ignore them) |
| Web server | Apache with `.htaccess` (`AllowOverride All`) and `mod_rewrite` — standard on cPanel |
| SSL | AutoSSL / Let's Encrypt (free in cPanel) |
| Nothing else | No Node.js, Redis, Supervisor, Docker, systemd or Composer needed |

Server-level access is **not** required for anything in this guide.

---

## 1. Folder layout on the server

```
/home/CPUSER/
├── icsspk-events/          ← upload the whole project here (NOT inside public_html)
│   ├── app/  bootstrap/  config/  database/  resources/  bin/  storage/
│   ├── .env                ← created in step 8, lives here
│   └── public/             ← the domain's document root points HERE
└── public_html/            ← untouched (or used by the fallback in section 14)
```

`CPUSER` is your cPanel username. Nothing in the code hard-codes it.

---

## 2. Create the MySQL database

cPanel → **Databases → MySQL® Database Wizard**

1. **Step 1 — Create A Database:** enter `icsspk` → *Next Step*.
   cPanel prefixes it, e.g. `CPUSER_icsspk`. Note the full name.

## 3. Create the MySQL user

2. **Step 2 — Create Database Users:** username `icsspk`
   (becomes `CPUSER_icsspk`), generate a strong password → *Create User*.
   Save the password; it goes into `.env`.

## 4. Assign database privileges

3. **Step 3 — Add User to the Database:** tick **ALL PRIVILEGES**
   → *Make Changes*.

   The minimum the application needs is: SELECT, INSERT, UPDATE, DELETE,
   CREATE, ALTER, INDEX, DROP, REFERENCES, LOCK TABLES, CREATE TEMPORARY
   TABLES. ALL PRIVILEGES on this one database is fine.

## 5. Upload the project outside `public_html`

cPanel → **Files → File Manager**

1. Open your home directory (`/home/CPUSER`), **not** `public_html`.
2. *Upload* → upload the release zip (e.g. `icsspk-events-phase2.zip`).
3. Right-click the zip → *Extract* → extract to `/home/CPUSER`.
   The zip contains a top-level `icsspk-events/` folder, so this creates
   `/home/CPUSER/icsspk-events`.
4. Check that `/home/CPUSER/icsspk-events/public/index.php` exists.
5. Delete the zip.

In File Manager, tick **Settings → Show Hidden Files (dotfiles)** so you can
see `.htaccess`, `.env.example` and `.user.ini`.

### File permissions

| Path | Permission |
|---|---|
| Folders | `755` |
| Files | `644` |
| `storage/` and all subfolders | `755` (must be writable by your account — it is, by default) |
| `public/media/` | `755` (writable; uploads arrive in Phase 4) |
| `.env` | `600` (created in step 8) |

Never use `777`.

## 6. Point the domain's document root at `public/`

cPanel → **Domains → Domains**

- **Subdomain or addon domain** (e.g. `events.example.org`): *Create A New
  Domain* (or *Manage* an existing one) and set **Document Root** to
  `icsspk-events/public`, then *Update*.
- **Primary domain:** most current cPanel versions let you change the
  primary domain's document root in the same screen (*Manage* →
  *Document Root*). If yours does not, use the fallback in section 14.

## 7. Select PHP 8.3 and enable extensions

1. cPanel → **Software → MultiPHP Manager**: tick the domain → PHP Version
   **8.3** (or newer) → *Apply*.
2. Extensions, depending on your host:
   - **CloudLinux ("Select PHP Version")**: tick `pdo`, `pdo_mysql`,
     `mysqlnd`, `curl`, `gd`, `fileinfo`, `openssl`, `mbstring`, `json`,
     `intl` → *Save*.
   - **EasyApache (no selector shown)**: these extensions are normally
     already on; ask your host if any are missing.
3. **MultiPHP INI Editor** (optional — `public/.user.ini` already sets
   these): `display_errors = Off`, `upload_max_filesize = 6M`,
   `post_max_size = 8M`, `session.use_strict_mode = 1`.

## 8. Create `.env`

1. In File Manager open `/home/CPUSER/icsspk-events`.
2. Copy `.env.example` to `.env` (right-click → *Copy* → name `.env`).
3. Right-click `.env` → *Edit* and set:

```ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://events.example.org      ; your real domain, no trailing slash
APP_TIMEZONE=Africa/Nairobi
APP_FORCE_HTTPS=true

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=CPUSER_icsspk
DB_USERNAME=CPUSER_icsspk
DB_PASSWORD=the-password-from-step-3

LOG_LEVEL=error
INTASEND_ENV=sandbox                    ; leave IntaSend values empty until Phase 8
```

4. Right-click `.env` → *Change Permissions* → `600`.

`.env` sits outside the web root, so it can never be downloaded. Never
put it in `public/` or `public_html/`.

## 9. Create the database tables (migrations)

Choose **one** method.

**A. phpMyAdmin (no SSH)**
1. cPanel → **Databases → phpMyAdmin** → select `CPUSER_icsspk` on the left.
2. *Import* → *Choose File* → `database/install.sql` from the project
   (download it from File Manager first, or use the copy from the zip).
3. *Import*. You should see 30 tables.
   `install.sql` also records every migration as applied, so later updates
   can use `bin/migrate.php` normally.

**B. Cron job (no SSH)** — runs the migrator once:
1. cPanel → **Advanced → Cron Jobs** → *Add New Cron Job*.
2. Common setting: *Once Per Minute*.
3. Command:
   ```
   /usr/local/bin/php /home/CPUSER/icsspk-events/bin/migrate.php >> /home/CPUSER/icsspk-events/storage/logs/cron.log 2>&1
   ```
4. Wait one minute, check `storage/logs/cron.log` for `Applied 29 migration(s).`
5. **Delete the cron job.**

**C. SSH / Terminal (if available)**
```bash
cd ~/icsspk-events
php bin/migrate.php          # apply
php bin/migrate.php --status # verify: every line says APPLIED
```

## 10. Run the base seed

The seed creates the admin roles, permissions and ICSSPK branding defaults.
It never creates an admin account and never overwrites settings you changed.

- **Used phpMyAdmin (9A)?** Already done — `install.sql` includes it.
- **Cron (no SSH):** as 9B with the command
  `/usr/local/bin/php /home/CPUSER/icsspk-events/bin/seed.php >> /home/CPUSER/icsspk-events/storage/logs/cron.log 2>&1`
  → expect `Base seed OK: 3 roles, 27 permissions, 34 settings.` → delete the cron job.
- **SSH:** `php bin/seed.php`

Do **not** run `--demo` in production. It is refused when `APP_ENV=production`.

## 11. Enable SSL

1. cPanel → **Security → SSL/TLS Status** → select the domain →
   *Run AutoSSL*. Wait until it shows a valid certificate.
2. With `APP_FORCE_HTTPS=true`, plain `http://` requests redirect to the
   `APP_URL` host over HTTPS.
3. Once HTTPS has worked for a few days, turn on HSTS: `SECURITY_HSTS=true`
   in `.env`. (Leave it off until then: HSTS is hard to undo.)

## 12. Test the site

| Check | Expected |
|---|---|
| `https://DOMAIN/` | ICSSPK-branded home page (navy `#294455`, red `#ED1B24`) |
| `https://DOMAIN/health` | `{"status":"ok","database":"ok"}` |
| `https://DOMAIN/awards`, `/faq`, `/contact` | Pages load; no PHP errors |
| `https://DOMAIN/.env` | 403 or 404 (never the file) |
| `https://DOMAIN/.user.ini` | 403 or 404 |
| `https://DOMAIN/app/`, `/config/app.php` | 404 (they are not in the web root) |
| `https://DOMAIN/robots.txt` | Contains `Sitemap: https://DOMAIN/sitemap.xml` |
| Browser dev tools → Network | No requests to Google Fonts or any CDN |

If a page shows *"Something went wrong… Reference: ABC123"*, look up the
reference in `storage/logs/app-YYYY-MM-DD.log`.

## 13. Hosts without URL rewriting (`/index.php/...` fallback)

If `https://DOMAIN/` works but `https://DOMAIN/awards` returns an Apache
404 while `https://DOMAIN/index.php/awards` works, `mod_rewrite` or
`.htaccess` overrides are disabled.

1. Ask the host to enable `mod_rewrite` and `AllowOverride All` (the proper fix).
2. Temporary workaround: set `APP_URL=https://DOMAIN/index.php` in `.env`.
   Every generated link then uses `/index.php/...`, while CSS, JS, images
   and fonts are still served directly.

If the whole site returns *500 Internal Server Error* right after upload,
the host may forbid the `Options` directive: delete the `Options -Indexes`
line in `public/.htaccess`.

## 14. Fallback: the host forces `public_html` as the document root

Use this only if you cannot change the document root (step 6).

```
/home/CPUSER/
├── icsspk-events/          ← everything EXCEPT the contents of public/
└── public_html/            ← the CONTENTS of public/ go here
    ├── index.php  .htaccess  .user.ini  assets/  media/
    └── app-root.php        ← tells index.php where the project lives
```

1. Upload and extract the project to `/home/CPUSER/icsspk-events` (step 5).
2. Move **the contents** of `icsspk-events/public/` into `public_html/`
   (`index.php`, `.htaccess`, `.user.ini`, `assets/`, `media/`,
   `app-root.php.example`). Keep hidden files.
3. In `public_html/` rename `app-root.php.example` to `app-root.php`. Its
   default (`dirname(__DIR__) . '/icsspk-events'`) already points at the
   sibling project folder, so no username needs typing. Edit it only if you
   used a different folder name.
4. Continue from step 7.

`app/`, `config/`, `database/`, `storage/` and `.env` stay outside
`public_html`, so they are never web-accessible. `public_html/.htaccess`
only allows `index.php` to execute, and `app-root.php` cannot be requested.
When you update, remember that `public/` files now live in `public_html/`.

## 15. Composer

Production does **not** use Composer or a `vendor/` folder: the
application ships its own autoloader (`app/autoload.php`) and has no
third-party runtime packages. Don't upload a `vendor/` folder.
`composer.json` exists only for developer convenience (`composer test`
runs `php tests/run.php`). If a future phase ever adds a development tool,
run `composer install` on your own computer, never on the server.

## 16. Cron jobs (from Phase 8 — not needed yet)

Find your PHP binary path first: in cPanel **Cron Jobs** it is usually
`/usr/local/bin/php`; on EasyApache hosts the version-specific binary is
`/opt/cpanel/ea-php83/root/usr/bin/php`.

| Schedule | Command | Purpose |
|---|---|---|
| `* * * * *` | `/usr/local/bin/php /home/CPUSER/icsspk-events/bin/cron-webhooks.php >> /home/CPUSER/icsspk-events/storage/logs/cron.log 2>&1` | Retry stored IntaSend webhook events |
| `*/5 * * * *` | `/usr/local/bin/php /home/CPUSER/icsspk-events/bin/cron-payments.php >> …/cron.log 2>&1` | Check stale PROCESSING payments with IntaSend, then expire |
| `15 * * * *` | `/usr/local/bin/php /home/CPUSER/icsspk-events/bin/cron-reconcile.php >> …/cron.log 2>&1` | Payment/vote reconciliation sweep |
| `30 2 * * *` | `/usr/local/bin/php /home/CPUSER/icsspk-events/bin/cron-maintenance.php >> …/cron.log 2>&1` | Prune rate-limit/login-attempt rows and old logs |

These scripts are created in later phases. Every `bin/` script refuses to
run from a browser.

## 17. IntaSend webhook (Phase 8)

The webhook URL to enter in the IntaSend dashboard will be
`https://DOMAIN/webhooks/intasend`, with the challenge string equal to
`INTASEND_WEBHOOK_CHALLENGE` in `.env`. The full procedure is added to
`docs/PAYMENTS.md` in Phase 8.

## 18. Updating to a new version

1. Back up the database (phpMyAdmin → *Export*) and `.env`.
2. Upload and extract the new release over the project folder. Don't
   overwrite `.env`, `storage/` or `public/media/`.
3. Run migrations (9B cron or 9C SSH). The runner applies only new
   migrations and refuses to run if an already-applied migration file was
   changed.

## 19. Backups

- **Database:** daily cPanel backup or phpMyAdmin export. It holds all
  payments, votes and audit logs.
- **Uploaded images:** `public/media/` (or `public_html/media/`).
- **Configuration:** keep a copy of `.env` somewhere safe and separate from
  database backups. It contains secrets.
- **Logs:** `storage/logs/` is optional to back up. Rotate or delete old logs.

## Production checklist

- [ ] Project is outside `public_html`; document root is `…/public`
- [ ] `.env` present, permission `600`, `APP_ENV=production`, `APP_DEBUG=false`
- [ ] `APP_URL` is the exact `https://` domain
- [ ] 30 tables exist; `php bin/migrate.php --status` (or `schema_migrations`) shows 29 applied
- [ ] Base seed run: 3 roles, 27 permissions, 34 settings
- [ ] No demo data (`SELECT COUNT(*) FROM awards WHERE slug LIKE 'demo-%'` → 0)
- [ ] AutoSSL certificate valid; HTTP redirects to HTTPS
- [ ] `/health` returns `ok`; `/.env` is not downloadable
- [ ] `storage/` writable; `storage/logs` contains no errors after browsing the site
