# Installing ProbizCRM

Two routes. Pick one.

- **[Browser install](#browser-install)** — upload a ZIP, open a page, answer six
  screens. No terminal, no commands. Works on cPanel shared hosting.
- **[Terminal install](#terminal-install)** — faster and more controllable if you
  have SSH.

Both produce the same installation.

---

## Browser install

### What you need first

Create these in cPanel before you start. Both are web forms, not commands.

1. **A database.** cPanel → *MySQL Databases*. Create a database, create a user,
   then add the user to the database with **All Privileges**. Write down all
   three values.
2. **PHP 8.2 or newer** on the domain. cPanel → *Select PHP Version*. While you
   are there, tick these extensions if they are not already on: `pdo_mysql`,
   `mbstring`, `openssl`, `curl`, `fileinfo`, `bcmath`, `phar`, `zip`, `tokenizer`. The
   installer checks all of them and tells you which are missing.

MySQL 8.0+ or MariaDB 10.6+ is required. Tenant isolation is enforced with
composite foreign keys, and older servers handle them differently enough that
the guarantee would not hold.

### Steps

**1. Upload and extract.**
cPanel → *File Manager*. Go to the folder your domain serves — usually
`public_html`, or `public_html/probizcrm` for a subfolder install. Upload
`ProbizCRM.zip`, then right-click it and choose **Extract**.

**2. Set folder permissions.**
Select `storage` and `bootstrap/cache`, right-click, **Change Permissions**, set
to **755**, and tick *Recurse into subdirectories*. If your host runs PHP as a
different user, 775 may be needed instead — the installer will tell you.

**3. Open the installer.**
Visit `https://yourdomain.com/install.php`. If you installed into a subfolder,
it is `https://yourdomain.com/probizcrm/install.php`.

**4. Work through the six screens.**

| Screen | What happens |
| --- | --- |
| Server check | Confirms PHP version, extensions and folder permissions |
| Dependencies | Downloads the framework using the bundled Composer, in-page |
| Database | Tests your credentials before accepting them |
| Your site | Address, time zone, currency and how email is sent |
| Administrator | Your platform account |
| Security | Enrols two-factor and shows your recovery codes |

The dependency step is the slow one — one to four minutes, roughly 60 MB. The
page will look frozen. Leave the tab open.

**5. Do the three things the final screen asks.**

*Set up the scheduler.* cPanel → *Cron Jobs*. Choose "Once per minute" and paste
the command the installer gives you. It looks like:

```
curl -s https://yourdomain.com/system/schedule/abc123... >/dev/null 2>&1
```

This is what expires trials and retries failed payments. Skip it and neither
happens. If your host does not offer cron, a free service such as cron-job.org
can fetch that URL on the same schedule.

*Point the domain at `public/`.* cPanel → *Domains* → edit the document root so
it ends in `/public`. The package includes a root `.htaccess` that makes the
site work without this, but pointing it properly keeps `.env` — which holds your
database password and application key — outside the web root entirely rather
than relying on rewrite rules to hide it.

*Delete the installer.* One button on the final screen.

### If a step fails

**"Missing" against an extension.** cPanel → *Select PHP Version* → tick it →
*Save*. Then click **Check again**. Almost all of these are a checkbox.

**A folder is not writable.** File Manager → right-click → *Change Permissions*
→ 755, recursing into subdirectories. If that does not do it, try 775.

**"Your requirements could not be resolved to an installable set of packages."**
A package conflict, not a server fault. The message names the packages. If it
mentions `laravel/tinker`, you are running a build from before 2 September 2026 —
tinker had no Laravel 13 release and was wrongly listed as a hard requirement.
Replace `composer.json` with the current one and retry.

**The dependency step times out.** Your host caps `max_execution_time` or
`memory_limit` too low. Ask them to raise both, then retry — the installer picks
up where it left off. If they will not, install `vendor/` another way: run
`composer install` on your own computer (Composer has a normal Windows
installer), zip the resulting `vendor` folder, upload and extract it beside
`composer.json`, then reload the installer. It detects the folder and skips the
step.

**"Could not connect" on the database screen.** Nine times in ten the user has
not been added to the database with All Privileges. Check that in *MySQL
Databases*. On most cPanel hosts the host value is `localhost`, not an IP.

**The database already contains tables.** Use an empty one. If you are
deliberately reinstalling, tick the confirmation box.

**"Call to undefined function symlink()".** Fixed in 0.3.3 — your host disables
that function. Re-upload the current package. Everything the installer had
already done (schema, seed data, your admin account) is preserved, and running
the install step again is safe.

**A blank white page.** Look at `storage/logs/`. Almost always a permissions
problem on `storage`.

---

## Terminal install

```bash
# 1. Get the code onto the server
unzip ProbizCRM.zip && cd probizcrm

# 2. Dependencies
php composer.phar install --no-dev --optimize-autoloader

# 3. Configure
cp .env.example .env
php artisan key:generate
# edit .env: DB_*, APP_URL, MAIL_*

# 4. Schema and seed data
php artisan migrate --force
PLATFORM_ADMIN_EMAIL=you@probizcrm.com php artisan db:seed --force

# 5. Permissions and the public link
chmod -R 775 storage bootstrap/cache
php artisan storage:link
```

Then point the web server at `public/`:

```nginx
server {
    listen 443 ssl http2;
    server_name probizcrm.com;
    root /var/www/probizcrm/public;
    index index.php;

    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }
    location ~ /\.(?!well-known).* { deny all; }
}
```

Cron, once a minute:

```
* * * * * cd /var/www/probizcrm && php artisan schedule:run >> /dev/null 2>&1
```

With SSH you also get a real queue worker, which is better than the `sync`
driver the browser installer configures:

```bash
# .env
QUEUE_CONNECTION=database

php artisan queue:work --queue=high,default --tries=3 --max-time=3600
```

Keep it alive with supervisor or systemd.

Your first platform admin cannot sign in until two-factor is enrolled. Enrol it
once:

```bash
php artisan probiz:enrol-admin you@probizcrm.com
```

That prints the enrolment key and your recovery codes. (`php artisan tinker`
would also work, but tinker has no Laravel 13 release yet and is no longer a
dependency.)

---

## After either route

Sign-in addresses:

- Platform console — `/platform/login`
- Customer sign-up — `/register`
- Customer sign-in — `/login`

### What the browser install configures differently

| Setting | Browser | Terminal | Why |
| --- | --- | --- | --- |
| Queue | `sync` | `database` or `redis` | No worker can be started without a shell, so queued work runs inside the request instead. Slower, but email actually sends. |
| Cache / sessions | `database` | `redis` | Redis is rarely available on shared hosting. |
| Scheduler | HTTP endpoint | `artisan schedule:run` | Same tasks, triggered by cron fetching a URL. |
| Billing gateway | `manual` | your choice | Nothing to configure until you have gateway credentials. |

None of this is permanent. Edit `.env` and the app picks up the change.

### Updating later

Upload the new files over the old ones, keeping your `.env` and `storage`
folder. Then either visit `/install.php` (it detects an existing installation and
offers to run only the pending migrations) or, with a shell:

```bash
php composer.phar install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan db:seed --class=Database\\Seeders\\PermissionSeeder --force
php artisan config:cache && php artisan route:cache
```

Re-running `PermissionSeeder` is how a new permission reaches an existing
installation. It is idempotent, and it warns rather than deletes when it finds a
permission in the database that is no longer in the config.

### Common runtime errors

**`MissingTenantContextException`.** Working as designed. Tenant queries fail
closed rather than returning every organisation's rows. It means some code
queried tenant data without establishing which tenant. Report it.

**Emails never arrive.** On a browser install the queue is `sync`, so mail sends
during the request — check `storage/logs/` for the actual SMTP error. On a
terminal install, check the worker is running.

**Trials never expire.** The scheduler is not being called. Verify by visiting
the schedule URL in a browser; it should print `ok`.
