# Pishon — cPanel Split-Document-Root Deployment

How to run **Pishon Educational Management** on cPanel with the Apache main-domain
document root fixed at `public_html`, while the Laravel application lives in a
separate folder beside it. This keeps browser URLs clean (`https://pishonedu.com.ng`)
and keeps tenant subdomains (`https://{slug}.pishonedu.com.ng`) working from the
same application.

The split is needed because cPanel normally serves the main domain from
`/home/USER/public_html`, and we do not change that document root. Instead
`public_html` holds only the *contents* Laravel would normally put in
`public/` (front controller, `.htaccess`, CSS/JS build, images), and its
`index.php` bootstraps the application one folder up.

---

## 1. Target layout

```
/home/USER/                       (e.g. /home/olunaths)
├── school-platform/              ← the Laravel application (one copy, never duplicated)
│   ├── app/ bootstrap/ config/ database/ resources/ routes/...
│   ├── vendor/                   ← composer install --no-dev on the server
│   ├── .env                      ← APP_URL, APP_BASE_DOMAIN, DB creds, keys
│   ├── storage/
│   │   ├── app/public/           ← uploaded files (symlinked into public_html)
│   │   └── framework/ bootstrap/ logs/
│   ├── public/
│   │   ├── build/                ← Vite manifest + compiled assets (symlink to public_html/build)
│   │   └── index.php             ← canoncial Laravel front controller (not served by Apache here)
│   └── artisan
└── public_html/                  ← Apache main-domain document root (FIXED, unchanged)
    ├── index.php                 ← DEPLOYMENT front controller (bootstraps ../school-platform)
    ├── .htaccess                 ← standard Laravel rewrite rules
    ├── build/                    ← Vite CSS/JS assets (served root-relative as /build/...)
    ├── images/                   ← landing-page images (served root-relative as /images/...)
    ├── robots.txt
    ├── favicon.ico
    └── storage -> /home/USER/school-platform/storage/app/public   (symlink)
```

**Why this shape**

- cPanel serves `public_html` as the docroot; anything outside it is not directly
  reachable, but PHP *can* `require` files outside the docroot. That is exactly
  what the deployment `index.php` does.
- The physical app, `.env`, `storage/`, and `vendor/` stay outside the docroot —
  never accessible from the web (defense in depth).
- All generated URLs are root-relative (`/build/...`, `/images/...`,
  `/school/...`) or absolute against `APP_URL`, so no install path ever leaks
  into a URL.

---

## 2. The deployment front controller

`deployment/public_html/index.php` is generated and mirrors Laravel 12's
canonical `public/index.php` exactly, except the two application paths are
relative to the sibling app folder (no `/home/USER` hard-coding — it works for
any hosting account):

```php
$appBase = __DIR__.'/../school-platform';   // change only if the app folder is renamed

if (file_exists($maintenance = $appBase.'/storage/framework/maintenance.php')) {
    require $maintenance;                    // maintenance mode still works
}
require $appBase.'/vendor/autoload.php';
$app = require_once $appBase.'/bootstrap/app.php';
$app->handleRequest(Request::capture());
```

- It is **relative**, so the same file is valid on `olunaths` or any account.
- The **canonical `school-platform/public/index.php` is untouched** and continues
  to drive local XAMPP dev (`/school-cms/school-platform/public`). It is simply
  not used by Apache in the split layout.
- `deployment/public_html/.htaccess` is the standard Laravel rewrite file;
  `public_html/index.php` therefore works exactly like a normal Laravel docroot.

`php -l` is run on every PHP file in the package as part of the release check
(see §7).

---

## 3. Assets: build, images, uploads

### CSS/JS (`build/`)
- `@vite()` resolves asset URLs from `public/build/manifest.json` (i.e.
  `school-platform/public/build`) and emits `/build/app-xxx.js`-style root-relative
  URLs. Apache serves those from `public_html/build`.
- Two locations therefore need the compiled assets:
  - `public_html/build` — what HTTP actually serves.
  - `school-platform/public/build` — what PHP reads for the manifest.
- **Recommended**: keep ONE copy as a symlink
  `ln -sfn /home/USER/public_html/build /home/USER/school-platform/public/build`.
  (The deployment package contains `build/` in both folders already; the symlink
  dedupes them. If you prefer no symlink, keep both copies and re-upload/rebuild
  together on each release.)
- If `public/build` is missing locally, run `npm install && npm run build`
  before packaging — the builder warns about this.

### Images + robots/favicon
- Copied verbatim into `public_html/` by the builder. URLs are `/images/...`.

### Uploads (`storage/app/public`)
- `php artisan storage:link` creates `school-platform/public/storage →
  storage/app/public` — that link is NOT served by Apache in this layout.
- The link Apache needs is the one at the docroot:
  `ln -sfn /home/USER/school-platform/storage/app/public /home/USER/public_html/storage`
  (create it from SSH or cPanel Terminal; delete a pre-existing `public_html/storage`
  folder first if any).

---

## 4. `.env` (no install path is encoded)

Use `.env.example` as the template (it is production-ready). Fill in:

| Key                    | Value                          |
|------------------------|--------------------------------|
| `APP_ENV`              | `production`                   |
| `APP_DEBUG`            | `false`                        |
| `APP_KEY`              | `php artisan key:generate`     |
| `APP_URL`              | `https://pishonedu.com.ng`     |
| `APP_BASE_DOMAIN`      | `pishonedu.com.ng`             |
| `SESSION_DOMAIN`       | `null` (host-scoped cookies = tenant isolation; never `.pishonedu.com.ng`) |
| `SESSION_SECURE_COOKIE`| `true` (requires HTTPS)        |
| `DB_*`                 | your cPanel MySQL database      |
| `MAIL_*`               | real SMTP (never `log`)         |
| gateway keys           | Paystack/Flutterwave live keys  |

> `APP_URL`/`APP_BASE_DOMAIN` are the only URL source of truth. Nothing in the
> app code contains `/school-cms`, `/school-platform`, or `/public` — verified by
> audit (Step 4/7 of the task). After any change to `APP_BASE_DOMAIN`, rebuild
> `php artisan route:cache` (the tenant subdomain route is baked literally at
> cache time).

File permissions: `.env` `600`, owned by the cPanel user; `storage/` and
`bootstrap/cache/` writable (`chmod -R 775`).

---

## 5. Tenant subdomain compatibility (unchanged)

The split layout does **not** touch the multi-tenant architecture:

- `routes/tenant.php` still registers the wildcard
  `Route::domain('{schoolSub}.'.strtolower(config('app.base_domain')))` group,
  required from `routes/web.php`. On production that resolves at runtime from
  `APP_BASE_DOMAIN=pishonedu.com.ng`.
- The one Laravel app serves the apex (landing + account area) **and** every
  `{slug}.pishonedu.com.ng` tenant website. Apache just hands every host to the
  same `public_html/index.php`.
- Unknown/reserved subdomains get the branded *School not found* 404; custom
  school domains redirect their bare root to the path-based home.
- Live wiring (separate from this package): wildcard DNS `*.pishonedu.com.ng` +
  apex, and a wildcard TLS certificate (AutoSSL wildcard or Let's Encrypt).
  Without HTTPS, `SESSION_SECURE_COOKIE=true` will break logins.

### 5.1 The three production school subdomains

A school is reachable on `{slug}.{base_domain}` **by default**, and on any other
hostname once `schools.domain` is set — `TenantResolver` matches the `domain`
column *before* the `{slug}` pattern. That is how a school keeps its slug (and
every `/school/{slug}` path URL) while being published on a branded hostname.

The required mapping, applied after `php artisan migrate --force`:

| School | Slug (unchanged) | Published hostname |
|---|---|---|
| Greenfield Academy | `greenfield-academy` | `greenfieldcollege.pishonedu.com.ng` |
| Sunrise International School | `sunrise-international` | `sunriseinternational.pishonedu.com.ng` |
| Royal Heritage International School | `royalheritage` | `royalheritage.pishonedu.com.ng` |

```bash
cd /home/olunaths/school-platform
php artisan school:set-domains greenfield-academy    greenfieldcollege
php artisan school:set-domains sunrise-international sunriseinternational
php artisan school:set-domains royalheritage         royalheritage
```

The command is idempotent (safe to re-run) and refuses to overwrite a hostname
already assigned to another school. Add `--dry-run` to preview. Accepts either
a slug or a numeric ID, and the domain with or without the `.pishonedu.com.ng`
suffix.

> `royalheritage` already matches its slug and would resolve regardless; the
> command is still listed so the intended production mapping is explicit and
> auditable in the database.

Without these rows the three hostnames return the branded *School not found*
404 — the DNS wildcard and TLS certificate alone are not enough, because
Laravel resolves the tenant from the `schools` table.

### 5.2 Public website URLs vs authenticated app URLs

This application has two URL families and they must not be mixed:

| Purpose | Helper | Example |
|---|---|---|
| Public school website (nav, cards, SEO, sitemap) | `tenant_url()` | `https://greenfieldcollege.pishonedu.com.ng/about` |
| Authenticated app (admin/staff/portal dashboards) | `route()` | `https://pishonedu.com.ng/school/1` |

The dashboards are path-based routes (`school/{schoolId}/...`) with no
tenant-subdomain equivalent. Generating them with `tenant_url()` produces a
tenant host with no matching route, which 404s. `tests/Feature/Auth/AuthenticatedRedirectTargetTest.php`
pins this split; keep post-login redirects on `route()`.

---

## 6. Build the release package

From the repo root (Windows, `C:\xampp\htdocs\school-cms\school-platform`):

```
powershell -ExecutionPolicy Bypass -File deployment\build-package.ps1
```

Produces `deployment/out/` (gitignored — a generated artifact, re-created per
release; it is **never** a second maintained copy of the app):

- `out/school-platform` → the application (dev/build/runtime junk and `.env*`
  excluded; `vendor/` and `node_modules/` left out — run `composer install` on
  the server).
- `out/public_html` → static assets + deployment `index.php`/`.htaccess`.
- `out/README-UPLOAD.txt` → upload + post-install checklist.

Options: `-Zip` to also write `school-platform.zip`/`public_html.zip`;
`-Method GitArchive` for a reproducible release cut from the last commit.

> The workspace root is `C:\xampp\htdocs\school-cms`; deploy only the
> `school-platform/` subfolder. The builder derives the application folder name
> from the repo (currently `school-platform`) and the front controller matches
> that name — it must stay `school-platform` on the server.

---

## 7. Server deployment steps

1. **Upload**
   - `out/school-platform` → `/home/USER/school-platform`
   - `out/public_html/*` → `/home/USER/public_html` (merge; overwrite
     `index.php`, `.htaccess`, `build/`, `images/`, favicon, `robots.txt`).
2. **PHP/composer** (via SSH or cPanel Terminal, as the cPanel user — never root,
   to avoid ownership mismatches):
   ```
   cd ~/school-platform
   composer install --no-dev --optimize-autoloader
   ```
3. **Environment**:
   ```
   cp .env.example .env
   php -r "copy('.env.example','.env');"  # or File Manager if no SSH
   # edit .env (see §4), then:
   php artisan key:generate
   ```
4. **Migrate**: `php artisan migrate --force`
5. **Publish the three school subdomains** (see §5.1) — required, otherwise the
   tenant hostnames resolve to the branded *School not found* 404:
   ```
   php artisan school:set-domains greenfield-academy    greenfieldcollege
   php artisan school:set-domains sunrise-international sunriseinternational
   php artisan school:set-domains royalheritage         royalheritage
   ```
6. **Symlinks**:
   ```
   php artisan storage:link
   ln -sfn /home/USER/school-platform/storage/app/public /home/USER/public_html/storage
   ln -sfn /home/USER/public_html/build           /home/USER/school-platform/public/build
   ```
7. **Permissions**:
   ```
   chmod -R 775 storage bootstrap/cache
   chmod 600 .env
   ```
8. **Caches** (production): `php artisan optimize:clear` then
   `php artisan config:cache`, `php artisan route:cache`, `php artisan view:cache`.
8. **Release sanity checks** (run locally before uploading AND on the server):
   ```
   php -l deployment/public_html/index.php        # front controller syntax
   php artisan route:list                          # {schoolSub} group present
   php artisan route:cache && php artisan route:clear
   php artisan test                                # full suite
   ```
9. **Verify live**:
   - `curl -I https://pishonedu.com.ng` → 200, Laravel session cookie set.
   - Login at the landing “Account” / a platform admin URL.
   - Each school subdomain renders its own website (DNS/SSL ready):
     `https://greenfieldcollege.pishonedu.com.ng`,
     `https://sunriseinternational.pishonedu.com.ng`,
     `https://royalheritage.pishonedu.com.ng`.
   - Sign in as a school admin, teacher and student and confirm each lands on a
     real dashboard — **not** a 404. Dashboards are path-based
     (`/school/{id}/...`); see §5.2.
   - Upload a file in a school's CMS and confirm it appears at
     `https://{slug}.pishonedu.com.ng/storage/...`.
10. **Background jobs** (see `docs/deployment-runbook.md`):
    - Cron: `* * * * * /usr/local/bin/php /home/USER/school-platform/artisan schedule:run >> /dev/null 2>&1`
    - Queue worker: `php /home/USER/school-platform/artisan queue:work --tries=3`

### Rollback
Keep the previous release (rename the old `school-platform` to
`school-platform.bak` and restore the old `public_html` files from File Manager /
the `public_html.zip`). That alone reverts the site; the `.env` and DB are
untouched.

---

## 8. Development safety (what does NOT change)

- Local dev keeps working exactly as before:
  `http://localhost/school-cms/school-platform/public/` (canonical
  `public/index.php`, `APP_BASE_DOMAIN=platform.test`).
- No app code, routes, controllers, views, or migrations were changed for this
  layout. The only churn is: the generated `deployment/public_html/index.php` and
  `.htaccess`, the builder `deployment/build-package.ps1`, and docs.
- No `/home/olunaths` literal appears anywhere in the repo; the front controller
  is path-relative. The `.env` remains gitignored and untouched on dev.
- `deployment/out/` is gitignored (build artifact) and export-excluded
  (`.gitattributes`) so it cannot leak into `git archive` releases.

---

## 9. Out of scope (do separately)

1. **DNS wildcard + apex records** and **wildcard TLS** for `*.pishonedu.com.ng`
   (cPanel *Wildcard Subdomain* + AutoSSL / Let's Encrypt).
2. cPanel MySQL database/user creation and credentials.
3. SMTP account for `MAIL_*`.
4. Payment gateway live keys + webhook URLs (see runbook).