# Deployment

This guide gets WedFlow Atelier running on three deployment targets, in the
order most CodeCanyon buyers will use them:

1. [Shared hosting (cPanel / Plesk / DirectAdmin)](#1-shared-hosting) — the most common path. Two routes: the **web installer wizard** (recommended), or the **manual cPanel File Manager** route if you don't have SSH/Terminal.
2. [VPS / dedicated server (Ubuntu + Nginx)](#2-vps--dedicated-server) — recommended once you're past ~50 active weddings.
3. [Managed PaaS (Forge / Ploi / Cleavr / Cloudways)](#3-managed-paas) — fastest path if you're already paying for one.

> **You'll need `wedflow-1.2.0.zip` (or later) from your CodeCanyon purchase before you start.**

---

## 1. Shared hosting

WedFlow runs comfortably on standard shared hosting (HostGator, Bluehost,
Hostinger, GoDaddy, A2, SiteGround, NameCheap, etc.) **as long as**:

- PHP 8.3 or 8.4 is available (most hosts let you switch in cPanel *MultiPHP Manager*)
- You can create a MySQL or PostgreSQL database (or have SSH access to `touch` an SQLite file)
- You have File Manager access
- You can add at least one cron job

If those four boxes tick, you're set.

### 1A. Pre-flight check (do this BEFORE uploading anything)

In cPanel:

1. **MultiPHP Manager** → ensure your domain runs PHP **8.3+** (8.4 also works).
2. **MultiPHP INI Editor** → confirm these extensions are enabled (most are by default):
   `pdo`, `pdo_mysql`, `mbstring`, `xml`, `bcmath`, `fileinfo`, `tokenizer`,
   `gd` *or* `imagick`, `openssl`, `curl`, `zip`.
3. **MySQL Databases** → create a database, a user, add the user to the
   database with **ALL PRIVILEGES**. Note the full names — cPanel prefixes
   both with your account name (e.g. `cpaneluser_wedflow`).
4. **SSH Access** → if your plan includes it, enable it. SSH speeds things up
   massively, but **it's not required** — every command below has a cPanel
   File Manager equivalent.

### 1B. Upload and extract the zip

1. Download `wedflow-1.2.0.zip` from your CodeCanyon purchase.
2. In cPanel **File Manager**, navigate to your account's home directory
   (one level *above* `public_html`).
3. Upload `wedflow-1.2.0.zip`.
4. Right-click the zip → **Extract**. The default extracts into a folder
   matching the zip name. Rename it to `wedflow/` for clarity (right-click → **Rename**).
5. Your layout should now look like:
   ```
   /home/cpaneluser/
   ├── wedflow/             ← the Laravel app
   │   ├── app/
   │   ├── bootstrap/
   │   ├── config/
   │   ├── database/
   │   ├── public/          ← the web root (we'll point Apache here)
   │   │   └── build/       ← pre-built Vite assets (you don't need Node)
   │   ├── resources/
   │   ├── routes/
   │   ├── storage/
   │   ├── artisan
   │   ├── composer.json
   │   └── .env.example
   └── public_html/         ← what Apache serves by default
   ```

> ⚠️ **Verify the extraction was complete.** Some browser-based extractors silently skip files in deep subtrees. The zip has **~300 files**. Count what you got:
>
> - Right-click `wedflow/` → **Properties** (or **Compress**) and check the file count
> - If you see significantly fewer than ~300 files, the extraction was partial — re-extract or download the zip again.

### 1C. Choose your layout — A or B

The single biggest source of confusion. Pick one and commit to it.

#### Layout A — Change the document root (recommended, most secure)

If your host allows it (cPanel → **Domains** → edit domain → change Document
Root), point your domain (or subdomain) at:

```
/home/cpaneluser/wedflow/public
```

Done. Nothing else to move. Skip to step 1D.

#### Layout B — `public_html/` bridge (when document root is locked)

If your host won't let you change the document root, you bridge through `public_html/`. **Read this carefully** — this is the layout that bit us during the live demo install.

1. **Empty `public_html/`** (move anything you care about aside, or delete it if empty).
2. **Move the entire contents of `wedflow/public/`** (not the parent `public/` folder — its contents) into `public_html/`. You should end up with:
   ```
   /home/cpaneluser/
   ├── wedflow/                  ← Laravel app (no /public anymore)
   │   ├── app/
   │   ├── bootstrap/
   │   ├── database/
   │   ├── storage/
   │   ├── vendor/  (after composer install in step 1E)
   │   └── ...
   └── public_html/               ← was wedflow/public/
       ├── index.php              ← the front controller
       ├── build/                 ← pre-built Vite assets
       │   ├── manifest.json
       │   └── assets/
       ├── favicon.ico
       └── .htaccess
   ```
3. **Edit `public_html/index.php`** — change the two `require` paths from `__DIR__.'/../'` to point at `../wedflow/`:

   ```php
   require __DIR__.'/../wedflow/vendor/autoload.php';
   $app = require_once __DIR__.'/../wedflow/bootstrap/app.php';
   ```

> 🚨 **Critical:** do not put `wedflow/` *inside* `public_html/`. If you do, your `.env`, `storage/logs/`, and `database/` become publicly accessible — anyone can browse to `https://yourdomain.com/wedflow/.env` and read every secret. If you accidentally end up in that layout (we did during this guide's first writing), see the "Lock down `wedflow/` if it's inside `public_html/`" section in [Post-launch hardening](#1k-post-launch-hardening) below.

### 1D. Install Composer dependencies (one command)

You need `vendor/` populated before Laravel can boot. Two paths:

#### With SSH or cPanel Terminal

```bash
cd ~/wedflow
composer install --optimize-autoloader --no-dev
```

If `composer` isn't on the PATH (rare on shared hosts):

```bash
cd ~/wedflow
curl -sS https://getcomposer.org/installer | php
php composer.phar install --optimize-autoloader --no-dev
```

#### Without SSH/Terminal (cPanel File Manager only)

If your host forbids both, the only way to install dependencies is to:

1. Run `composer install --optimize-autoloader --no-dev` **on your local machine** inside the extracted zip folder
2. Upload the resulting `vendor/` folder via SFTP or cPanel File Manager
3. Use **Compress** in File Manager on the local `vendor/` first if it's large — uploads a single zip then extract server-side

The `composer.lock` ships in the zip so the dependency versions are deterministic between your machine and the server.

### 1E. Configure `.env` and generate APP_KEY (recommended: use the wizard)

You now have two paths. **The wizard is easier and won't let you mistype anything.**

#### Path 1 — Web installer wizard (recommended)

1. Visit `https://yourdomain.com/install` in your browser
2. **Step 1 (Welcome):** the wizard checks PHP version + extensions; auto-creates `.env` from `.env.example`; auto-runs `php artisan key:generate`. Click **Continue**.
3. **Step 2 (Database):** pick driver (sqlite / mysql / pgsql), enter credentials. The wizard **tests the connection** before writing to `.env`. Click **Save & Continue**.
4. **Step 3 (Admin):** enter your admin name, email, password. This creates the first user and grants them admin privileges.
5. **Step 4 (Finish):** the wizard runs migrations (`migrate --force`), optionally seeds demo data (toggle on/off), creates the storage symlink, sets `APP_DEBUG=false`, and writes a lock file so `/install` becomes inaccessible afterwards.

When done, you land on `/login`. Sign in with the admin account you just created.

#### Path 2 — Manual `.env` + CLI

If the wizard doesn't suit you (or you want full control):

```bash
cd ~/wedflow
cp .env.example .env
php artisan key:generate
```

Then edit `.env` (see the [reference values](#1f-env-reference) section below). Then:

```bash
php artisan migrate --seed --force
php artisan storage:link
php artisan config:cache
```

### 1F. `.env` reference

For Path 2, the minimum production set is:

```ini
APP_NAME="Your Brand"
APP_ENV=production
APP_KEY=base64:...                      # auto-generated by key:generate
APP_DEBUG=false                          # IMPORTANT — leak-proof
APP_URL=https://yourdomain.com           # NO trailing slash

DB_CONNECTION=mysql                      # lowercase — case matters
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=cpaneluser_wedflow
DB_USERNAME=cpaneluser_wedflow
DB_PASSWORD=...

# Defaults ship as 'file' so the installer can boot.
# After /install completes, you may switch to 'database' for multi-server durability.
SESSION_DRIVER=file
CACHE_STORE=file
QUEUE_CONNECTION=database
FILESYSTEM_DISK=public

# Cookies should match the bare hostname
SESSION_DOMAIN=.yourdomain.com
```

> Mail, OAuth, Stripe, and S3 credentials **don't need to live in `.env`** — set them from `/admin/settings` after you sign in. Database-backed values override `.env`, by design, so non-technical operators never have to touch a file.

### 1G. Storage symlink

If you used the wizard, this is done. If you went Path 2:

```bash
php artisan storage:link
```

Required for guest-uploaded photos and admin-uploaded branding to render in the browser.

**If your shared host blocks symlinks** (some do), use the cPanel fallback: in File Manager, navigate to `public_html/storage/` (or `wedflow/public/storage/`) and check whether it's a symlink. If not, manually create the path and add a `.htaccess` that rewrites requests to `../../storage/app/public/`.

### 1H. Cron — one line drives everything

In cPanel → **Cron Jobs**, add one entry:

```
* * * * * cd /home/cpaneluser/wedflow && php artisan schedule:run >> /dev/null 2>&1
```

The Laravel scheduler runs every minute and drives:

- The queue worker for email and Stripe webhooks (Laravel auto-runs `schedule:work` on hosts without a daemon)
- Daily cleanup (expired share tokens, old `stripe_events`)

> If your host allows a long-running daemon (some cPanel plans expose this via "Application Manager"), running `php artisan queue:work --tries=3 --timeout=60` as a separate process is slightly more responsive. Otherwise the cron is fine.

### 1I. Permissions

```bash
chmod -R 775 storage bootstrap/cache
```

Or via File Manager: select `storage/` and `bootstrap/cache/` → top toolbar **Permissions** → set to 0755 (folders) and 0644 (files), recursive.

If you see "permission denied" errors after deploy, the fix is almost always this.

### 1J. Optimize for production

```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
```

Re-run after any config or route change. To clear them temporarily: `php artisan optimize:clear`.

> Don't run `config:cache` BEFORE running the installer wizard — it'll bake the empty `.env` defaults into a cache that masks the wizard's writes.

### 1K. Post-launch hardening

The site is up. Three things, in priority order, **before you tell anyone the URL exists**.

#### Change the seeded passwords (immediate)

If you ran `migrate --seed --force` or used the wizard's demo-data toggle, you have:

- `admin@wedflow.test` / `password`
- `couple@wedflow.test` / `password`

Both are documented in this guide. Sign in as each, change the password from `/profile`. Or delete the demo couple entirely if you don't need it.

#### Confirm the installer is locked

The wizard creates `storage/installed` automatically when you click Finish on Step 4. If you skipped the wizard (Path 2), create the file yourself:

- **SSH:** `touch storage/installed && chmod 644 storage/installed`
- **File Manager:** navigate to `storage/` → top toolbar **+ File** → name it exactly `installed` (no extension) → Create

Verify by visiting `https://yourdomain.com/install` — it should redirect to `/`.

#### Lock down `wedflow/` if it's inside `public_html/`

This is the layout we ended up in during the live install — and it leaks `.env`. If your structure looks like `public_html/wedflow/...`, do one of these:

**Best (move out of `public_html/`):**

```bash
cd /home/cpaneluser
mv public_html/wedflow .                # move source outside web root
cp -r wedflow/public/. public_html/     # copy public assets to web root
# Edit public_html/index.php — change the two require lines to:
#   require __DIR__.'/../wedflow/vendor/autoload.php';
#   $app = require_once __DIR__.'/../wedflow/bootstrap/app.php';
```

**Quick mitigation (`.htaccess` deny):**

In File Manager, create `/home/cpaneluser/public_html/wedflow/.htaccess` with:

```apacheconf
<RequireAll>
    Require all denied
</RequireAll>
```

Test by browsing to `https://yourdomain.com/wedflow/.env` — you should get a **403 Forbidden**. If you still see file contents, your host doesn't honour `.htaccess` denials and you must use the "Best" path.

#### Force HTTPS

Most hosts ship Let's Encrypt via cPanel → **SSL/TLS Status** → AutoSSL. Enable it, then add this to the top of `public_html/.htaccess`:

```apacheconf
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</IfModule>
```

### 1L. Configure the platform from `/admin/settings`

Sign in as your admin user and walk through:

1. **Branding** — app name, logo, support email, accent colour
2. **Email Delivery** — Resend or SMTP credentials, click **Send test email** to verify. **⚠️ Required before users can sign up** — registration is gated by mandatory email verification. Without working email, new sign-ups will be stranded.
3. **File Storage** — local (default) or S3/R2 with separate public URL
4. **Google Sign-In** — optional OAuth credentials
5. **Billing (Stripe)** — Secret Key, Webhook Signing Secret, Premium price ID, Planner price ID

Settings here override anything in `.env`, by design, so non-technical operators never have to touch a file.

### 1M. Final verification

- [ ] `https://yourdomain.com/` → atelier landing page renders (espresso ground, gold accent)
- [ ] `/login` accepts your changed admin password
- [ ] `/admin/settings → Email Delivery → Send test email` arrives in your inbox
- [ ] Upload a test branding logo from `/admin/settings → Branding` → confirm it renders at the URL it shows you
- [ ] `/pricing` shows three plan cards; clicking "Upgrade" hits Stripe (if configured)
- [ ] `/install` redirects to `/` (lock file present)
- [ ] `https://yourdomain.com/wedflow/.env` returns 403 (if you went Layout B with `wedflow/` inside `public_html/`)

---

## 1N. Running a public demo (optional)

If you're standing up a *public* demo at a marketing URL (the way the author
runs `wedflow.site`), enable demo mode so visitor traffic can't change your
configured platform settings, and add a daily reset so visitor data doesn't
accumulate.

### Enable demo mode

Add to `.env`:

```ini
WEDFLOW_DEMO_MODE=true
```

Then clear caches:

```bash
php artisan config:clear
```

What changes when demo mode is on:

- A **gold "DEMO MODE" banner** appears at the top of every authenticated page
- All `POST` / `PUT` / `PATCH` / `DELETE` to `/admin/*` routes are blocked with a flash explaining the demo state — visitors who log in as the seeded admin can read every page but can't change settings, promote/demote admins, change plans, or delete users
- The `Inertia` shared prop `app.demo_mode` is `true`, so future feature work can add additional demo-aware UX

Reads still work everywhere — buyers see exactly what the running app looks like, they just can't break it.

### Daily reset cron

Wipe everything visitors did and restore the seeded demo data each midnight:

```
0 0 * * * cd /home/cpaneluser/wedflow && php artisan wedflow:reset-demo >> storage/logs/demo-reset.log 2>&1
```

The `wedflow:reset-demo` command:

- Runs `migrate:fresh --seed --force` (drops every table, re-creates schema, re-runs seeders)
- Empties `storage/app/public/branding/`, `/shares/`, `/weddings/` so visitor-uploaded files don't accumulate
- Re-issues the `storage:link` (some hosts break it during full migrations)
- Clears Laravel caches

The command **refuses to run if `WEDFLOW_DEMO_MODE` is not enabled** — pass `--force` to override if you really need to.

> Always test the reset command manually first (`php artisan wedflow:reset-demo`) before adding it to cron so you know the seeders complete successfully and the reset takes ~5–15 seconds rather than minutes.

### Envato preview iframe compatibility

CodeCanyon wraps your demo URL in an iframe at:

```
https://preview.codecanyon.net/item/<item-slug>/full_screen_preview/<id>
```

Three things normally block this iframe — all handled automatically when `WEDFLOW_DEMO_MODE=true`:

| What | Why it normally breaks | What demo mode does |
| --- | --- | --- |
| **X-Frame-Options / CSP frame-ancestors** | Apache or cPanel security plugins often set `X-Frame-Options: SAMEORIGIN`, refusing the iframe outright | `EnvatoPreviewHeaders` middleware strips X-Frame-Options and sets `CSP: frame-ancestors 'self' https://preview.codecanyon.net https://codecanyon.net https://*.envato.com https://*.envatousercontent.com` |
| **`SameSite=Lax` cookies** | Browsers refuse to send Lax cookies inside a cross-origin iframe — session lost on first navigation, app appears logged out forever | `AppServiceProvider` forces `SESSION_SAME_SITE=none` and `SESSION_SECURE=true` for the demo's lifetime |
| **Third-party cookie blocking (CHIPS)** | Chrome 118+ and Safari 17+ block cookies in cross-origin iframes by default, even with `SameSite=None + Secure` — login appears to do nothing because the session cookie is dropped on the redirect after POST `/login` | `PartitionedCookies` middleware appends the `Partitioned` attribute (CHIPS opt-in) to every cookie, scoping it to the (top-frame, embedded-frame) pair. Cookies remain usable inside the iframe but cannot be used for cross-site tracking |
| **Referrer leakage** | The preview iframe could leak your real demo URL via Referer headers | `Referrer-Policy: strict-origin-when-cross-origin` is set so only the origin (no path) is exposed |

**You don't have to do anything for this** — just leave `WEDFLOW_DEMO_MODE=true` set and Envato's preview will work. None of these relaxations apply to production installs (where `WEDFLOW_DEMO_MODE` is unset/false), so your buyers' deploys stay strict.

### Verify the preview works (before submitting)

Once your demo is live with `WEDFLOW_DEMO_MODE=true`, you can simulate Envato's iframe locally without involving Envato at all. Save this as `iframe-test.html` and open it in a browser:

```html
<!DOCTYPE html>
<html><body style="margin:0;">
<iframe src="https://wedflow.site" style="width:100vw;height:100vh;border:0;"></iframe>
</body></html>
```

If the iframe renders your atelier landing page and you can navigate / sign in inside it, Envato's preview will work. If you see a "Refused to display" message in the browser console, the frame-ancestors header didn't take — see the troubleshooting matrix.

### Demo URL in CodeCanyon listing

When you submit on CodeCanyon, your listing's **Demo URL** field is what buyers click to try the app before buying. Recommended pattern:

```
https://yourdomain.com                       (your atelier landing)
admin@wedflow.test / password                (admin sandbox login)
couple@wedflow.test / password               (couple sandbox login)
```

Document this in your CodeCanyon item description as well, since the field
itself is just one URL.

---

## 2. VPS / dedicated server

Recommended once you're past ~50 active weddings. A $5–10/mo droplet runs 500+
weddings comfortably.

### One-shot installer (Ubuntu 24.04)

```bash
# As root on a fresh server
apt update && apt -y upgrade
apt install -y nginx php8.3-fpm php8.3-cli php8.3-mbstring php8.3-xml \
               php8.3-bcmath php8.3-pdo php8.3-mysql php8.3-zip \
               php8.3-curl php8.3-gd composer mysql-server git certbot \
               python3-certbot-nginx unzip
adduser --disabled-password --gecos "" wedflow
usermod -aG www-data wedflow
```

### App setup

```bash
sudo -u wedflow -i
cd ~
unzip /path/to/wedflow-1.2.0.zip -d ./wedflow
cd wedflow
composer install --optimize-autoloader --no-dev
mysql -u root -e "CREATE DATABASE wedflow; CREATE USER 'wedflow'@'localhost' IDENTIFIED BY 'CHANGE-ME'; GRANT ALL ON wedflow.* TO 'wedflow'@'localhost'; FLUSH PRIVILEGES;"
# Then visit https://yourdomain.com/install in a browser to use the wizard
# OR continue with CLI:
cp .env.example .env
php artisan key:generate
# edit .env: APP_URL, DB_*, SESSION_DOMAIN
php artisan migrate --seed --force
php artisan storage:link
php artisan config:cache && php artisan route:cache && php artisan view:cache
chown -R wedflow:www-data storage bootstrap/cache
chmod -R 775 storage bootstrap/cache
```

### Nginx server block

```nginx
# /etc/nginx/sites-available/wedflow.conf
server {
    listen 80;
    server_name yourdomain.com;
    root /home/wedflow/wedflow/public;
    index index.php;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    charset utf-8;
    client_max_body_size 32m;

    location / { try_files $uri $uri/ /index.php?$query_string; }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* { deny all; }
}
```

```bash
ln -s /etc/nginx/sites-available/wedflow.conf /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d yourdomain.com
```

### systemd queue worker (instead of cron)

```ini
# /etc/systemd/system/wedflow-queue.service
[Unit]
Description=WedFlow queue worker
After=network.target mysql.service

[Service]
User=wedflow
Group=wedflow
Restart=always
RestartSec=5
ExecStart=/usr/bin/php /home/wedflow/wedflow/artisan queue:work --tries=3 --timeout=120 --sleep=3

[Install]
WantedBy=multi-user.target
```

```bash
systemctl daemon-reload
systemctl enable --now wedflow-queue
```

Plus the scheduler cron once:

```bash
crontab -u wedflow -e
# Add:
* * * * * cd /home/wedflow/wedflow && php artisan schedule:run >> /dev/null 2>&1
```

---

## 3. Managed PaaS

### Laravel Forge / Ploi / Cleavr

1. Provision a fresh Ubuntu droplet via the panel.
2. Create a new site, pointing at `public/`.
3. Git push your repo (private or buyer-modified fork).
4. In the panel: enable the **Daemon** for `php artisan queue:work`.
5. Enable the **Scheduler** (it sets up the `* * * * *` cron automatically).
6. Set `.env` from the web UI (Forge stores it encrypted).
7. Click **Deploy**. The panel runs `composer install --no-dev`, `php artisan migrate --force`, and clears caches.

### Cloudways

WedFlow runs on Cloudways' Laravel optimised stack. Use their SSH terminal for the commands in the VPS section. Their **Cron Job Manager** handles the scheduler, and **Supervisor** handles the queue worker (preferred over cron).

### Vercel

**Not recommended.** Vercel's filesystem is ephemeral, so guest-uploaded photos in `storage/app/public/` disappear between deploys. Use a host with persistent disk, or first swap WedFlow's storage to S3/R2 via `/admin/settings → File Storage` so nothing depends on local disk.

---

## Troubleshooting matrix

Every one of these was encountered (and fixed) during a real shared-hosting install. If you hit any of them, the fix is short.

| Symptom | Cause | Fix |
| --- | --- | --- |
| 500, blank page, log says `Class "Laravel\Pail\PailServiceProvider" not found` | A stale `bootstrap/cache/packages.php` references a dev-only package. Usually happens if you ran `composer install` (with dev deps) and then re-ran `composer install --no-dev`. | In File Manager, delete `bootstrap/cache/packages.php` and `bootstrap/cache/services.php` (keep `.gitignore`). The next request will regenerate them clean. |
| 500, log says `No application encryption key has been specified` | `APP_KEY` is empty in `.env`, OR config is cached against an old `.env` that had no key. | Run `php artisan key:generate --force` then `php artisan config:clear`. If no shell: re-run the `/install` wizard which auto-generates the key. |
| 500, log says `Database file at path [...] does not exist` | `DB_CONNECTION=sqlite` but no SQLite file exists. | Best: switch to MySQL via `/install`. Quick: in File Manager, create empty file at `database/database.sqlite`. |
| 500, log says `Database connection [MySQL] not configured` | Connection name is case-sensitive — must be lowercase `mysql`. | Edit `.env`: `DB_CONNECTION=mysql` (lowercase). Clear config cache. |
| 500, log says `Base table or view not found: sessions doesn't exist` | Migrations haven't run yet, OR you switched `SESSION_DRIVER` to `database` before re-migrating. | Run `php artisan migrate --seed --force`. If you really want `SESSION_DRIVER=database`, switch it back to `file` until after the install wizard completes, then flip it. |
| 500 during migration, `Specified key was too long; max key length is 1000 bytes` | If you added your own indexed `varchar` columns longer than 191 chars on shared MySQL with the legacy 767-byte InnoDB key prefix, they overflow. WedFlow's own migrations stay within bounds. | Cap your custom indexed string columns at 191 chars, or upgrade to MySQL 8 / MariaDB 10.3+ which removes the limit. |
| 500, log says `Vite manifest not found at: ...public/build/manifest.json` | The `build/` folder is either missing entirely OR is in the wrong location relative to your chosen layout. | See the layout section in 1C. Layout A: `build/` should be at `wedflow/public/build/`. Layout B: `build/` should be at `public_html/build/` (and the contents of `wedflow/public/` should have been moved into `public_html/`, not the `wedflow/` folder itself). |
| Blank white page, no error in log, browser dev tools show `/build/assets/*.js → 404` | Vite is generating asset URLs the browser can't fetch — usually because Layout B was done half-way (manifest in one place, assets in another). | Make sure `public_html/build/` contains both `manifest.json` AND the `assets/` subfolder. Don't move the asset folder away from where Apache can serve `/build/assets/...`. |
| "419 PAGE EXPIRED" on every form | `SESSION_DOMAIN` wrong or `APP_URL` doesn't match the URL in the browser bar. | Set `SESSION_DOMAIN=.yourdomain.com` and `APP_URL=https://yourdomain.com` (no trailing slash). Clear config cache. |
| Uploaded photos don't render in the gallery | `php artisan storage:link` not run, or shared host blocks symlinks. | Re-run `storage:link`. If blocked, add the `.htaccess` rewrite from §1G. |
| Stripe webhook receives 401 / 419 | Reverse proxy stripping `Stripe-Signature` header. | The webhook route at `/billing/webhook` is already CSRF-exempted in `app/Http/Middleware/VerifyCsrfToken.php`. If still failing, check your reverse-proxy config. |
| `ENOSPC` / disk full | `storage/logs/laravel.log` filled the disk. | Rotate the log, or add `LOG_DAYS=7` to `.env`. |
| Slow pages after deploy | Caches not regenerated. | `php artisan optimize:clear`, then re-run `config:cache route:cache view:cache event:cache`. |

For anything not in this matrix, the first line of `storage/logs/laravel.log` after the failure contains the answer. Include those lines (redacted) when you write to support.

---

## cPanel-only command reference

If you don't have SSH or cPanel Terminal, here's how to do every `artisan` command via the File Manager / cPanel UI:

| `artisan` command | cPanel equivalent |
| --- | --- |
| `php artisan view:clear` | Delete all `.php` files in `storage/framework/views/` |
| `php artisan cache:clear` | Delete contents of `storage/framework/cache/data/` |
| `php artisan config:clear` | Delete `bootstrap/cache/config.php` |
| `php artisan optimize:clear` | All four of the above |
| `php artisan key:generate` | Run the `/install` wizard (it auto-generates the key) |
| `php artisan migrate --seed --force` | Run the `/install` wizard (Step 4 does this) |
| `php artisan storage:link` | Run the `/install` wizard (also handled in Step 4) |
| Force PHP-FPM / OPcache flush | cPanel → **Select PHP Version** → toggle to a different version → Apply → toggle back → Apply |
| Run any one-off `artisan` command | cPanel → **Cron Jobs** → add a `* * * * *` schedule for `cd /home/cpaneluser/wedflow && php artisan COMMAND`, wait one minute, delete the cron |

---

*If you finished this guide and are running, take 60 seconds to do the [post-launch hardening](#1k-post-launch-hardening) checklist before publishing your URL anywhere.*
