# Shared cPanel Deployment Procedure

This runbook deploys City Crest Solar Concierge as a Laravel application with precompiled React assets and a cPanel MySQL database. Replace every uppercase placeholder before running a command.

## 1. Verify the hosting plan

Confirm all of the following before uploading:

- PHP 8.3+ is selectable for the domain and terminal/cron.
- PDO MySQL, cURL, OpenSSL, Mbstring, Tokenizer, XML, Ctype, JSON, BCMath, Fileinfo, and GD are enabled.
- GD can decode JPEG/PNG/WebP and encode PNG; verify with cPanel's PHP Info rather than assuming the base extension includes every format.
- MySQL 8+ or a compatible modern MariaDB release is available with InnoDB, foreign keys, JSON, and `utf8mb4`.
- Apache honors `.htaccess` and rewrite rules.
- The domain can point to the project's `public` directory. If it cannot, use the split-root procedure in section 5.
- SSL is active before enabling secure session cookies.
- `storage` and `bootstrap/cache` can be made writable by the PHP account.
- PHP may make outbound HTTPS requests to the configured background-removal provider.
- The account can create Laravel's `public/storage` symlink, or the host provides an equivalent safe mapping.
- Storage/inode quota covers both each private source upload and its public processed PNG.
- PHP upload size, POST size, memory limit, and request timeout allow one 10 MB source image plus synchronous background processing; the application deliberately never processes five images in one request.
- Composer 2/SSH is available, or `vendor` can be built using a compatible PHP environment and uploaded.

The application needs no Node process in production. Node and npm are needed only on the build machine.

## 2. Prepare a release locally

Start from the reviewed release branch/commit in a clean checkout. Do not include the local `.env`, database files, logs, `node_modules`, or backups.

```bash
composer install
npm ci
composer test
npm test
npm run typecheck
npm run lint
npm run build
composer validate --strict
composer audit
npm audit
```

The `npm run build` command creates `public/build`. That directory is normally excluded from Git and must be present in the upload artifact.

If the server has Composer, upload source plus `composer.json`/`composer.lock` and install there. If the server has no Composer, run this locally under PHP 8.3+ and include `vendor` in the upload:

```bash
composer install --no-dev --prefer-dist --optimize-autoloader
```

Do not include development-only `.env` values. Use `.env.production.example` as the production template.

## 3. Create the cPanel database

In **cPanel → MySQL Databases**:

1. Create a database, for example `CPANELUSER_citycrest`.
2. Create a dedicated user, for example `CPANELUSER_citycrest`.
3. Generate a long unique password.
4. Add that user to that database with the privileges needed by Laravel migrations and runtime operations. On standard cPanel this is commonly **All Privileges** for the application schema.
5. Record the exact cPanel-prefixed database name and username.

Do not reuse the cPanel account password or MySQL root. Laravel should have access only to its own schema.

## 4. Recommended deployment layout

The safest and simplest layout is:

```text
/home/CPANELUSER/city-crest/
├── app/
├── bootstrap/
├── config/
├── database/
├── public/              <- domain document root
├── resources/
├── routes/
├── storage/
├── vendor/
├── artisan
└── .env
```

Upload/extract the release to `/home/CPANELUSER/city-crest`. In **cPanel → Domains**, set the domain/subdomain document root to:

```text
/home/CPANELUSER/city-crest/public
```

Verify that hidden file `public/.htaccess` was uploaded. Never point the document root at the project root because that can expose source and secrets if server rules fail.

## 5. Split-root fallback for fixed `public_html`

Use this only when cPanel cannot change the document root.

Keep the Laravel application above the public web root:

```text
/home/CPANELUSER/city-crest-app/   <- private Laravel application
/home/CPANELUSER/public_html/      <- public files only
```

Copy the contents of `city-crest-app/public/` into `public_html/`, including `.htaccess`, `build`, and `images`. Then change only the two bootstrap paths in `public_html/index.php`:

```php
if (file_exists($maintenance = __DIR__.'/../city-crest-app/storage/framework/maintenance.php')) {
    require $maintenance;
}

require __DIR__.'/../city-crest-app/vendor/autoload.php';

$app = require_once __DIR__.'/../city-crest-app/bootstrap/app.php';
```

Keep the remaining Laravel 13 `public/index.php` code unchanged. The private app directory and public directory now form one release, so every deployment must update both atomically or under maintenance mode. Do not copy `.env`, `app`, `config`, `database`, `resources`, `routes`, `storage`, or `vendor` into `public_html`.

## 6. Create production environment configuration

Copy `.env.production.example` to `.env` in the private application root and set:

```dotenv
APP_ENV=production
APP_DEBUG=false
APP_URL=https://YOUR-DOMAIN.example

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=CPANELUSER_citycrest
DB_USERNAME=CPANELUSER_citycrest
DB_PASSWORD=YOUR_UNIQUE_DATABASE_PASSWORD

SESSION_SECURE_COOKIE=true
SESSION_ENCRYPT=true
SESSION_LIFETIME=60
SANCTUM_STATEFUL_DOMAINS=YOUR-DOMAIN.example,www.YOUR-DOMAIN.example
CORS_ALLOWED_ORIGINS=https://YOUR-DOMAIN.example,https://www.YOUR-DOMAIN.example
REQUEST_MAX_BYTES=1048576
UPLOAD_REQUEST_MAX_BYTES=12582912

BACKGROUND_REMOVAL_PROVIDER=removebg
REMOVE_BG_API_KEY=YOUR_PRIVATE_REMOVE_BG_API_KEY
REMOVE_BG_ENDPOINT=https://api.remove.bg/v1.0/removebg
REMOVE_BG_TIMEOUT=30
REMOVE_BG_MAX_RESPONSE_BYTES=20971520
```

Create the key on the server once:

```bash
php artisan key:generate
```

If cPanel exposes versioned CLI binaries, use its PHP 8.3 command, commonly `/usr/local/bin/ea-php83`, in place of `php`. Never rotate `APP_KEY` on an established production system without a migration plan; doing so invalidates encrypted values and sessions.

Keep `.env` out of `public`/`public_html`, source control, support tickets, and screenshots. File permissions should allow the account/PHP process to read it but not expose it broadly; `600` is appropriate where supported.

The remove.bg key is server-only and must never be copied into React/Vite variables. Production must use `removebg`; `fake` is reserved for local development and automated tests and is deliberately refused in production. A missing key does not break the shop, but new/replacement product uploads return a clear configuration error until it is supplied. Confirm the hosting firewall permits outbound HTTPS to the configured endpoint.

### Google OAuth

If Google sign-in will be live:

1. Create/select the Google OAuth web application.
2. Add the exact authorized redirect URI `https://YOUR-DOMAIN.example/auth/google/callback`.
3. Set `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, and the exact `GOOGLE_REDIRECT_URI` in `.env`.
4. Test both a new Google user and an existing-email association.

Without credentials, password authentication remains available; do not advertise/test the Google button as an enabled production integration.

## 7. Install dependencies on the server

If Composer is available:

```bash
cd /home/CPANELUSER/city-crest
composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction
```

If Composer is unavailable, upload the locally produced `vendor` directory. It must be built from the committed lockfile with a platform compatible with PHP 8.3. Never run `composer update` during a deployment.

No `npm install`, Vite server, `npm run dev`, or `npm run preview` command should run on production. Upload the already built `public/build` directory.

## 8. Set writable directories

From cPanel Terminal/SSH:

```bash
cd /home/CPANELUSER/city-crest
chmod -R ug+rwX storage bootstrap/cache
php artisan storage:link
```

Typical cPanel ownership already places PHP and the account in the correct user context. Avoid `777`. If permissions fail, use the host's documented ownership/group model rather than opening world-write access. Verify that `public/storage` resolves to `storage/app/public`, while `storage/app/product-originals` is not web-addressable. In a split-root deployment where symlinks are forbidden, arrange a host-approved mapping or synchronize only `storage/app/public` into `public_html/storage`; never copy private originals into the web root.

## 9. Migrate and seed

Back up before changing an existing database. Then run:

```bash
php artisan migrate --force
php artisan db:seed --force
```

The seeders are idempotent for roles, product/category slugs, bundle slugs, and setting keys. They refresh their managed catalogue children. On an established store, review future seeder changes before rerunning them so deliberate production catalogue edits are not overwritten.

The seeders do not create an admin account. Create the first operator through a controlled one-time process:

1. Register the intended user through the application so its password is correctly hashed and it receives `customer`.
2. In phpMyAdmin, find that exact email in `users` and the `admin` role ID in `roles`.
3. Insert the user's UUID and role ID into `role_user` with the current timestamp, using the unique pair only once.
4. Sign out/in and confirm `/admin` works.

Never ship the local staging administrator or a shared default password to production. Prefer a future protected invitation command/UI if administrators will be onboarded repeatedly.

## 10. Optimize production

After `.env`, dependencies, and migrations are correct:

```bash
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

These commands were verified against the application. Re-run them after any configuration or route deployment. If troubleshooting environment changes, run `php artisan optimize:clear`, correct the value, and rebuild caches.

The current `.env.production.example` uses `QUEUE_CONNECTION=sync`, so a queue worker is not required. No scheduled tasks are currently defined. If scheduling is added later, configure one cPanel Cron Job that runs Laravel's scheduler every minute using the correct PHP 8.3 binary.

## 11. Configure HTTPS and redirects

Issue/install the cPanel AutoSSL certificate first. Set `APP_URL` to the canonical HTTPS origin and `SESSION_SECURE_COOKIE=true`. Redirect HTTP to HTTPS through cPanel's supported redirect mechanism or a carefully reviewed Apache rule. Avoid redirect loops behind a proxy/CDN; configure trusted proxies if a reverse proxy is introduced.

Use one canonical host (`example.com` or `www.example.com`) and include every same-origin host that may serve the SPA in `SANCTUM_STATEFUL_DOMAINS`. Do not include schemes in that variable. Set `CORS_ALLOWED_ORIGINS` to the same approved HTTPS origins, including schemes, and never use `*` with credentialed requests. Confirm PHP `expose_php` is disabled; the supplied Apache rules also remove `X-Powered-By` and apply the static-resource security headers.

## 12. Release smoke tests

Use a private/incognito browser and verify:

1. `https://DOMAIN/api/health` returns JSON with `status: ok`.
2. `/`, `/services`, `/shop`, one `/shop/{slug}`, `/bundles`, `/builder`, `/cart`, and `/contact` render without console/network errors.
3. Product/category/bundle data loads from MySQL.
4. Adding a cart item survives reload.
5. A test inquiry is created and appears in `/admin/inquiries`.
6. Customer registration cannot access `/admin`.
7. An admin/staff account can access the admin pages and update an inquiry status.
8. Admin/staff can create and edit a controlled test product; the source lands privately and the public result is a 1200 × 1200 PNG with a black background.
9. Enable a percentage discount, verify original/sale/savings on home/shop/detail and discounted cart/WhatsApp totals, then repeat with a fixed amount and disabled state. Confirm `products.price` remains unchanged.
10. Upload four additional images one at a time, verify 5/5, sixth-image rejection, ordered public thumbnails, primary propagation, reorder, safe secondary/primary removal, automatic promotion, and zero-image prevention.
11. Product active/featured controls immediately affect `/shop` and the homepage; no more than four featured cards render.
12. In `/admin/bundles`, change a controlled bundle price, composition, capability label, recommendation threshold, and visibility; verify `/bundles` and `/builder`, then restore the production values.
13. In `/admin/builder`, change a controlled appliance and sizing parameter; verify the public calculation changes, then restore the production values.
14. Staff cannot access the settings API; an admin can change the WhatsApp number and cart/bundle/builder/contact links use the new destination without a deployment. Restore the production number after the test.
15. Logout prevents revisiting admin pages.
16. Mobile navigation, gallery thumbnail strip, and the admin tables/forms work at a narrow viewport.
17. Google OAuth works only if configured.
18. `storage/logs/laravel.log` contains no new production exceptions or provider credential leakage.

Deactivate/unfeature or clearly label the smoke-test product, and remove or label the smoke-test inquiry. Do not hard-delete products referenced by bundles and do not test with real payment data because the application has no payment flow.

## 13. Updates and maintenance releases

For each release:

1. Build and test a clean release locally.
2. Take/download a coordinated database and product-image backup and retain the prior code release.
3. Enable maintenance mode when the update is not backward compatible: `php artisan down --secret="RANDOM-BYPASS"`.
4. Deploy code, `vendor`, and `public/build` as one release.
5. Run `php artisan migrate --force`.
6. Clear/rebuild Laravel caches.
7. Disable maintenance mode with `php artisan up`.
8. Complete smoke tests before deleting the prior release.

Database migrations and code should be designed for backward-compatible rolling deployment where possible. See `docs/deployment/ROLLBACK.md` before deploying a destructive schema change.

## 14. Backup and rollback

Do not treat cPanel's existence as proof that backups are enabled. Schedule/download all of:

- the MySQL application database;
- the private production `.env` through a secure backup channel;
- `storage/app/product-originals`;
- `storage/app/public/products`.

The SQL dump is required to recover discount settings and each gallery's image order/primary relation. The two media trees must come from the same recovery point as that SQL dump; restoring only files or only `product_images` can leave broken references.

Keep encrypted copies outside the hosting account and perform restore drills. Full commands and decision points are in `docs/deployment/DATABASE-BACKUP.md` and `docs/deployment/ROLLBACK.md`.

## 15. Common failures

| Symptom | Likely cause | Action |
| --- | --- | --- |
| 500 on every route | Wrong PHP version/extensions, missing key/vendor, permissions | Check cPanel PHP 8.3, `storage/logs/laravel.log`, `.env`, `vendor`, writable dirs |
| Homepage works; deep links 404 | Missing `.htaccess` or rewrite support | Upload `public/.htaccess`; confirm `mod_rewrite`/AllowOverride |
| Vite manifest error | `public/build` missing or wrong release | Rebuild locally and upload entire `public/build` |
| Database connection error | cPanel prefix/host/password/privilege mismatch | Recheck exact DB name/user, assignment, host, and clear config cache |
| 419 on forms/login | HTTPS/cookie/domain/CSRF mismatch | Confirm canonical URL, secure cookie, Sanctum domains, browser cookies, proxy settings |
| Admin returns 403 | Signed-in user lacks admin/staff relation | Verify `users`, `roles`, and `role_user`; never weaken middleware |
| Product upload says provider is not configured | Provider/key missing or cached | Set `BACKGROUND_REMOVAL_PROVIDER=removebg` and the private key, then clear/rebuild config cache |
| Product processing fails | Provider/firewall/credit/timeout problem or corrupt image | Check safe Laravel log context, provider account and outbound HTTPS; retain the existing product image |
| Product PNG returns 404 | `public/storage` missing/wrong or split-root public storage not synchronized | Run `storage:link` or apply the documented host-safe mapping; never expose originals |
| Product decoder fails | GD lacks the uploaded format | Enable GD JPEG/PNG/WebP support or move to a compatible plan |
| Gallery stops mid-selection | Provider/host timeout affected one request | Completed images remain valid; review the safe status/log, correct the cause, and retry only failed images |
| Gallery path/order mismatch | Database and media were restored from different points | Restore MySQL plus both image trees from one coordinated backup; do not guess or renumber rows manually |
| Google redirect error | Callback or client credentials mismatch | Match the exact HTTPS callback in Google and `.env`; clear config cache |
| CSS/JS stale after update | Old build/cache/CDN | Deploy matching manifest/assets, clear Laravel/CDN/browser cache |

## 16. Go-live acceptance

Deployment is accepted only when the checklist is signed off, the backup can be located, the rollback owner is known, secrets are absent from the web root/repository, HTTPS is enforced, security headers/CSP/HSTS are present, debug is off, customer users cannot reach admin APIs, and the application passes the production smoke tests. Review the latest CI OWASP ZAP report before release and follow `docs/SECURITY-VAPT.md` for residual risks.
