# Fresh installation

> **STAGING CANDIDATE — pending coverage verification.** Do not use this
> checklist as a production-readiness claim. Complete the application's
> acceptance and security coverage review before uploading the ZIP.

This is a native fresh installation of the Laravel application in
`migration-laravel/app`. It is not an importer for the existing PostgreSQL
database or Clerk identity provider.

## 1. Build locally, never on the host

The build machine needs PHP 8.4+, Composer 2, npm/Node, and the PHP Zip
extension. The upstream Composer manifest allows `^8.3`, but this release's
installer and deployment contract require PHP 8.4+; PHP 8.2 is unsupported.
cPanel does **not** need SSH, Composer, npm, Node, or a long-running Node
process: the ZIP ships both production `vendor` and compiled assets.

From the repository root:

```bash
php migration-laravel/scripts/build-package.php --check-only
```

This creates a temporary production stage, installs Composer dependencies with
`--no-dev`, compiles Vite assets, removes build tools and runtime state, and
checks the resulting tree. It does not write a distributable ZIP. Inspect and
test that stage/package through the acceptance process first. Only after the
main release owner verifies the integrity checks should a package be written:

```bash
php migration-laravel/scripts/build-package.php --force
```

The ZIP is written to
`migration-laravel/build/repair-shop-laravel-package.zip`. Do not upload it
until the release owner has verified the staged package. Before transfer,
`unzip -l` should show `repair-shop-laravel/` and optional `public_html/`, and
no `.env`, `node_modules`, `tests`, `.git`, `*.map`, logs, session files,
`storage/installed.lock`, setup-code, `storage/app/installation/app-key`, or
generated `bootstrap/cache/*.php`.

The build is isolated deliberately: it copies the source first, then runs
Composer and npm only in that copy. A developer's existing `app/vendor` and
`app/node_modules` are not mutated.

## 2. Prepare cPanel before uploading

### PHP and extensions

Select PHP **8.4** for the domain/subdomain in cPanel MultiPHP Manager or
Select PHP Version. The wizard checks the following extensions:

| Installer check | PHP extension |
| --- | --- |
| PDO | `pdo` |
| PDO MySQL | `pdo_mysql` |
| OpenSSL | `openssl` |
| Mbstring | `mbstring` |
| Tokenizer | `tokenizer` |
| XML | `xml` |
| Ctype | `ctype` |
| JSON | `json` |
| Fileinfo | `fileinfo` |
| BCMath | `bcmath` |
| cURL | `curl` |

The locked Composer dependencies also require the normal PHP `filter`, `hash`,
and `session` capabilities. Enable them if the host exposes them separately.
The installer displays the actual PHP version and every failed extension;
correct all failures before continuing.

### Database

In cPanel **MySQL Databases**:

1. Create a MySQL 8 database, or use MariaDB 10.11 supplied by the host.
2. Create a dedicated database user with a strong unique password.
3. Grant that user the required privileges on that database.
4. Leave the database empty. Record the cPanel-prefixed database name,
   username, host (commonly `localhost`), and port (commonly `3306`).

The wizard cannot create users or databases. It only tests the credentials,
executes Laravel migrations, executes the required seeders, and creates the
administrator entered in the wizard. It never drops a database. Stop if the
target contains unrelated tables or records; do not point a fresh install at
an existing production database.

### HTTPS and backups

Issue and verify the domain's SSL certificate before setup. Use the HTTPS URL
for every installer request. Back up the existing web root and hosting files
before copying anything; preserve that backup for rollback. Never overwrite
existing `public_html` files without a dated backup and a deliberate review.

## 3. Upload outside `public_html`

The recommended layout is:

```text
/home/CPANEL_USER/
├── releases/
│   └── repair-shop-laravel-package.zip
├── repair-shop-laravel/
│   ├── app/
│   ├── bootstrap/
│   ├── config/
│   ├── database/
│   ├── routes/
│   ├── storage/
│   ├── vendor/
│   └── public/
└── public_html/                 # existing files; back up before bundle copy
```

Use cPanel File Manager to create `releases` outside `public_html` and upload
the ZIP. On a fresh home directory, extracting it into `$HOME` creates
`repair-shop-laravel` and the optional prebuilt `public_html` bundle. On an
existing account, back up `public_html` before extraction: archive tools may
merge or overwrite matching paths. Never blindly extract over a live site;
extract to a staging location or select only the private tree, then
intentionally copy public files after review. Do not extract the full archive
into `public_html`.

Set the domain document root to
`/home/CPANEL_USER/repair-shop-laravel/public` if the host allows a custom document
root. Confirm that the PHP handler is the same PHP 8.4 version selected above.

If this is the primary cPanel domain and its document root is fixed at
`/home/CPANEL_USER/public_html`, use the ZIP's optional `public_html/` bundle
instead of editing PHP:

1. Back up the existing `public_html` under a dated name and keep unrelated
   hosting files. The extraction itself never deletes or overwrites that
   directory.
2. After reviewing the backup, intentionally copy the optional bundle's
   contents into `public_html`; this is a deliberate file operation, not an
   automatic live-site replacement.
3. Keep `$HOME/repair-shop-laravel` beside `public_html` and private. The
   bundled wrapper checks that private folder and its `vendor` tree exist,
   returns a friendly HTTP 503 if they do not, and then requires
   `dirname(__DIR__).'/repair-shop-laravel/public/index.php'`.
4. Keep the bundled `.htaccess`, `preflight.php`, and compiled `build/`
   directory together with that wrapper. No user PHP edit is needed, and the
   private application's `public/index.php` remains unchanged.

Grant only the web-server account the required write access to `storage` and
`bootstrap/cache`. A practical starting point is directories `0750` and files
`0640`, with installer secret files and `.env` restricted to `0600` when the
host permits it. Keep `storage/app/installation` private. Never use `chmod
777`; reduce permissions again after diagnosing any host-specific issue.

## 4. Run the protected wizard

1. Visit `https://example.com/install/`.
2. The first request creates private bootstrap artifacts. In cPanel File
   Manager, read the ownership proof from the actual configured path:
   `repair-shop-laravel/storage/app/installation/setup-code`.
   This path comes from `app/config/installation.php`:
   `storage_path('app/installation/setup-code')`. The setup code is not
   displayed by the website. Do not put it in a ticket, shell history, or
   log.
3. Enter the code at `/install/begin`, pass the requirement checks, and use
   the cPanel MySQL host, port, database, user, and password.
4. Complete the application URL/name/timezone/locale step. Use the final
   HTTPS URL, not a temporary HTTP URL.
5. Create the first administrator with a unique password of at least 12
   characters containing upper- and lowercase letters, a number, and a
   symbol.
6. Confirm the final action only after checking the database name. The
   installer runs migrations, runs the empty required seeders, creates that
   administrator, writes production `.env`, verifies storage and URL, and
   creates `repair-shop-laravel/storage/installed.lock`.

The wizard does not create demo customers, admin records, or imported
production records. The only administrator created is the one entered during
this protected run. It does not create/drop the cPanel database or user.

After completion, refresh `/install/`: it must show the locked state. Leave
`storage/installed.lock` in place. Do not remove it or expose a reinstall/reset
route. Check the application home page and administrator login over HTTPS,
then review the host's PHP error log without putting logs in the package.

## 5. Secrets and private storage

The installer writes the managed `.env` at:

```text
/home/CPANEL_USER/repair-shop-laravel/.env
```

It writes the real `APP_KEY` there and uses a private bootstrap key under
`repair-shop-laravel/storage/app/installation/app-key` while the wizard is
unlocked.
Back up the final `APP_KEY`/`.env` through an encrypted, access-controlled
off-host process. Do not rotate `APP_KEY` during routine deployment. Losing it
can make encrypted application data and sessions unreadable. Keep `.env`,
`storage`, and all installation files outside the document root and never
commit or email them.

Private uploads use `storage/app/private`. Serve them through an authenticated
controller that checks authorization and streams the file; do not expose that
directory with a symlink. `php artisan storage:link` is optional only when the
application deliberately needs the separate public disk at
`storage/app/public`. cPanel File Manager can create a symlink if the host
allows it, but no symlink is required for a fresh install.

## 6. Data migration is a separate project

This wizard starts an empty MySQL/MariaDB schema. Existing PostgreSQL rows,
identifiers, files, and Clerk users are **not** imported automatically.
Importing those records requires a separate mapping, export, backup, dry run,
identity decision, and acceptance review. Do not paste an old PostgreSQL
connection string into the MySQL installer, and do not assume Clerk users are
local Laravel users.

## 7. Queues and cron

The installer writes `QUEUE_CONNECTION=sync`; therefore no queue worker or
cron entry is required for this fresh install. Do not add a pretend worker.

Only if the application owner intentionally changes the environment to a real
database/Redis queue, verifies the queue schema and job workload, and confirms
the selected cPanel PHP binary, use a bounded command such as:

```cron
* * * * * /usr/bin/flock -n /home/CPANEL_USER/.repair-shop-queue.lock /usr/local/bin/php /home/CPANEL_USER/repair-shop-laravel/artisan queue:work database --stop-when-empty --max-jobs=25 --max-time=50 --tries=3 --timeout=45 >/dev/null 2>&1
```

Replace both paths with values from the host. If `flock` or the PHP path is
not available, ask the host for its documented equivalents; do not remove the
bounded limits. Ensure `retry_after` exceeds the worker timeout and prevent
overlapping cron invocations. A queue cron is not evidence that unsupported
background work is ready.
