# MenuKosh — Deployment Guide

How to deploy MenuKosh, including the PWA / push / real-time features added in
this build. **Read Section 0 first** — the real-time and push features have a
hosting requirement your current host may not meet.

---

## 0. Read this first — hosting reality check ⚠️

Two of the features in this build need **persistent background processes**,
which a basic shared host (no SSH, deploy-via-`public/deploy.php`) cannot run:

| Feature | Needs | Works on shared hosting? |
|---|---|---|
| **PWA install, offline, icons** (Part 1) | Nothing special — static files | ✅ Yes |
| **Loud sound alert** (Part 3) | Nothing — runs in the browser | ✅ Yes |
| **Web push notifications** (Part 2) | A queue worker **OR** `QUEUE_CONNECTION=sync` | ⚠️ Yes, with `sync` (see 4a) |
| **Live order dashboard via Reverb** (Part 4) | A always-on WebSocket daemon (`reverb:start`) | ❌ **No** — needs a VPS or a host that allows long-running processes |

**The decision you need to make:** the live real-time dashboard (Reverb)
requires a server where you can keep `php artisan reverb:start` running 24/7.
That is not possible on a no-SSH shared host. Your options:

1. **Move MenuKosh to a VPS** (DigitalOcean, Hetzner, AWS Lightsail, a
   Cloudways/Ploi-managed server, etc.) — then everything in this guide works,
   including Reverb via Supervisor (Section 5). Recommended if live orders
   matter to you.
2. **Stay on shared hosting** — push notifications still work (via `sync`
   queue, Section 4a), the loud sound alert still works when a dashboard tab is
   open, and the PWA installs fine. But the "🟢 Live" instant-update dashboard
   will show "🔴 Offline" because it can't reach a Reverb server. ⚠️ Note: this
   build removed the old 10-second polling notifier and replaced it with
   Reverb — so on shared hosting, staff would need to refresh to see new orders
   until Reverb has a home. If you're staying on shared hosting for now, tell
   me and I'll add a polling fallback so the dashboard still updates without a
   daemon.

Everything below assumes a VPS for the Reverb parts; the plain deploy steps
(Sections 1–3) apply to any host.

---

## 1. Standard deploy steps (every release)

On a VPS with SSH:

```bash
cd /var/www/menukosh          # your project root

git pull                                          # get latest code
composer install --no-dev --optimize-autoloader   # prod dependencies only
php artisan migrate --force                        # apply new migrations
php artisan config:cache                           # cache config
php artisan route:cache                            # cache routes
php artisan view:cache                             # precompile blade
npm ci                                             # install exact JS deps
npm run build                                      # compile assets to public/build
php artisan queue:restart                          # tell queue workers to reload new code
```

If you deploy via `public/deploy.php` (shared hosting, no SSH), that script
already runs the cache/migrate steps — but it **cannot** run `composer install`,
`npm run build`, or `queue:restart`. On shared hosting you must upload the
already-built `public/build/` directory and `vendor/` from your local machine
(run `npm run build` and `composer install --no-dev` locally first, then upload).

---

## 2. Environment variables this build added

Make sure production `.env` has these (values from earlier build steps):

```env
# Web push (Part 2) — generate with: php artisan menukosh:generate-vapid
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_SUBJECT=mailto:admin@pinakra.com

# Reverb (Part 4) — only used if you run a Reverb server
REVERB_APP_ID=...
REVERB_APP_KEY=...
REVERB_APP_SECRET=...
REVERB_HOST="menukosh.pinakra.com"
REVERB_PORT=443
REVERB_SCHEME=https
REVERB_SERVER_HOST=127.0.0.1      # bind address for the daemon itself
REVERB_SERVER_PORT=8080

# Frontend build reads these (must be present at `npm run build` time)
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"

# Queue — see 4a
QUEUE_CONNECTION=database          # or "sync" on shared hosting
BROADCAST_CONNECTION=reverb        # or "log" if not running Reverb
```

⚠️ `VITE_*` variables are **baked into the compiled JS at build time**, not read
at runtime. If you change any `REVERB_*` value, you must re-run `npm run build`
and re-deploy `public/build/` — editing `.env` alone won't update the frontend.

---

## 3. Service worker version bump procedure

The service worker (`public/sw.js`) caches static assets. When you change
cached files (icons, offline page, or the SW logic itself), browsers won't pick
up the change until the cache name changes.

**To force every installed client to update:**

1. Open `public/sw.js`.
2. Bump the cache constant — change `MENUKOSH_CACHE_V1` to `MENUKOSH_CACHE_V2`
   (then `V3`, etc.) — update **both** the constant name and its string value.
3. Deploy.

On their next visit, each client's SW sees the new file, installs it, the
`activate` handler deletes the old cache, and (via the `SKIP_WAITING` +
`controllerchange` flow already wired into `partials/pwa-head.blade.php`) the
page reloads once automatically onto the new version. No user action needed.

You do **not** need to bump the version for normal Blade/controller/CSS
changes — only when something the SW actually caches changes shape.

---

## 4. When to re-submit to the Play Store vs. when changes go live automatically

The Android app is a **Trusted Web Activity** — a thin native wrapper that just
loads `https://menukosh.pinakra.com`. This means:

| You changed... | Play Store re-submit needed? |
|---|---|
| Any Blade view, controller, CSS, JS, menu logic, prices | ❌ No — it's live the moment you deploy to the server. The app loads your live site. |
| `manifest.json` name/colors/icons | ❌ No for behaviour, but the **installed** app keeps its original launcher icon/name until reinstalled. Fine to leave. |
| App package ID, target SDK, or you need a new signed build | ✅ Yes — rebuild in PWABuilder, bump the version code, upload a new `.aab` |
| Play listing text, screenshots, data safety | ✅ Yes — but that's a listing edit in Play Console, not a new build |

In practice, 95% of your updates are just server deploys and never touch the
Play Store. See `PLAY_STORE_SUBMISSION.md` for the build/upload process.

---

## 4a. Queue worker (for push notifications)

`SendPushNotification` is a queued job. You have two ways to run it:

**Option A — `sync` (shared hosting, no daemon):** set `QUEUE_CONNECTION=sync`.
The job runs inline during the order-placement HTTP request. Simple, no daemon
needed. Downside: sending push to many devices adds a little latency to the
diner's "order placed" response. Fine for small/medium restaurants.

**Option B — a real queue worker (VPS):** set `QUEUE_CONNECTION=database` (and
run `php artisan queue:table && php artisan migrate` once to create the jobs
table, if not already present). Then keep a worker running via Supervisor
(Section 5). Push sending happens in the background — no latency on the order
request.

---

## 5. Supervisor configs (VPS only)

Supervisor keeps the queue worker and Reverb daemon alive and restarts them if
they crash or the server reboots. Install: `sudo apt install supervisor`.

### 5a. Queue worker — `/etc/supervisor/conf.d/menukosh-queue.conf`

```ini
[program:menukosh-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/menukosh/artisan queue:work --sleep=3 --tries=3 --max-time=3600
directory=/var/www/menukosh
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/menukosh/storage/logs/queue-worker.log
stopwaitsecs=3600
```

### 5b. Reverb daemon — `/etc/supervisor/conf.d/menukosh-reverb.conf`

```ini
[program:menukosh-reverb]
command=php /var/www/menukosh/artisan reverb:start --host=127.0.0.1 --port=8080
directory=/var/www/menukosh
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/menukosh/storage/logs/reverb.log
stopwaitsecs=10
```

Bind Reverb to `127.0.0.1` (not `0.0.0.0`) and put your web server in front of
it — see Section 6. After adding/editing configs:

```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start menukosh-queue:* menukosh-reverb
sudo supervisorctl status              # verify both are RUNNING
```

After every deploy, reload the workers so they run the new code:
```bash
php artisan queue:restart              # queue workers pick up new code
sudo supervisorctl restart menukosh-reverb
```

### 5c. Systemd alternative (if Supervisor isn't available)

`/etc/systemd/system/menukosh-reverb.service`:

```ini
[Unit]
Description=MenuKosh Reverb WebSocket Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/menukosh
ExecStart=/usr/bin/php /var/www/menukosh/artisan reverb:start --host=127.0.0.1 --port=8080
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

`/etc/systemd/system/menukosh-queue.service`:

```ini
[Unit]
Description=MenuKosh Queue Worker
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/menukosh
ExecStart=/usr/bin/php /var/www/menukosh/artisan queue:work --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now menukosh-reverb menukosh-queue
sudo systemctl status menukosh-reverb menukosh-queue
```

---

## 6. Reverb behind Apache (WebSocket reverse proxy)

The frontend connects to `wss://menukosh.pinakra.com` on port 443; the Reverb
daemon listens on `127.0.0.1:8080`. Apache bridges them. Enable the proxy
modules and add to your HTTPS VirtualHost:

```bash
sudo a2enmod proxy proxy_http proxy_wstunnel rewrite
```

```apache
<VirtualHost *:443>
    ServerName menukosh.pinakra.com
    DocumentRoot /var/www/menukosh/public

    # Reverb WebSocket endpoint (Reverb serves the Pusher protocol under /app)
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} =websocket [NC]
    RewriteRule /app/(.*) ws://127.0.0.1:8080/app/$1 [P,L]

    ProxyPass /app/ http://127.0.0.1:8080/app/
    ProxyPassReverse /app/ http://127.0.0.1:8080/app/
    # Reverb's REST publish endpoint (server -> Reverb)
    ProxyPass /apps/ http://127.0.0.1:8080/apps/
    ProxyPassReverse /apps/ http://127.0.0.1:8080/apps/

    # ... your existing SSL + Directory config ...
</VirtualHost>
```

(Nginx equivalent uses a `location /app { proxy_pass; proxy_set_header Upgrade;
proxy_set_header Connection "upgrade"; }` block — ask if you're on Nginx.)

---

## 7. Cron / scheduler

MenuKosh doesn't define any scheduled tasks **yet**, so no cron is strictly
required today. But it's good practice to add Laravel's scheduler entry now so
future scheduled work (e.g. purging old push subscriptions, nightly reports)
just works. On a VPS, `crontab -e`:

```cron
* * * * * cd /var/www/menukosh && php artisan schedule:run >> /dev/null 2>&1
```

Most shared-hosting control panels (cPanel → Cron Jobs) offer the same — add
that one line pointing at your project's `artisan`.

---

## 8. Post-deploy smoke test

After any deploy, quickly confirm:

- [ ] Site loads at `https://menukosh.pinakra.com` and login works
- [ ] `https://menukosh.pinakra.com/manifest.json` returns 200
- [ ] `https://menukosh.pinakra.com/.well-known/assetlinks.json` returns 200 with `Content-Type: application/json`
- [ ] `https://menukosh.pinakra.com/privacy` loads
- [ ] (VPS only) `sudo supervisorctl status` shows queue + reverb RUNNING
- [ ] (VPS only) Dashboard shows "🟢 Live" in the header, not "🔴 Offline"

See `TESTING.md` for the full feature test matrix.

---

## 9. Security hardening — Phase 1 (env, secrets, transport)

### 9a. Production `.env`

Use [`.env.production.example`](.env.production.example) as the checklist —
it documents every value plus the reasoning (why `SESSION_DRIVER=database`,
why `APP_DEBUG=false` is non-negotiable, how to rotate `APP_KEY` safely).
Do not copy it verbatim; every `<ANGLE_BRACKET>` placeholder must be replaced
with a real, freshly-generated value.

### 9b. `composer install --no-dev --optimize-autoloader` deploy checklist

This is already Section 1 above; restated here for the security checklist:

```bash
composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan route:cache
php artisan view:cache
```

`--no-dev` matters: dev dependencies (Pail, Pint, Collision, Faker, PHPUnit)
should never ship to production — smaller attack surface, no accidental
`Faker` data generation or debug tooling reachable in prod.

### 9c. Apache — dotfile / sensitive-file blocking

Already implemented in [`public/.htaccess`](public/.htaccess): denies all
dotfiles (`.env`, `.git*`) and blocks `composer.json/.lock`, `package.json`,
`artisan`, `*.sql`, `*.log`, `*.bak`, `*.zip`, etc. by filename regardless of
extension tricks. No change needed — verify after any host migration that
`AllowOverride All` (or equivalent) is set so `.htaccess` is actually honored.

### 9d. Nginx equivalent (if you ever migrate off Apache)

Nginx does not read `.htaccess`. If MenuKosh ever moves to an nginx-fronted
host (e.g. a VPS with nginx + php-fpm instead of Apache), add this inside the
`server { }` block, alongside the standard Laravel front-controller config:

```nginx
server {
    listen 443 ssl http2;
    server_name menukosh.pinakra.com;
    root /var/www/menukosh/public;
    index index.php;

    # Deny all dotfiles (.env, .git, .htaccess, etc.)
    location ~ /\. {
        deny all;
        return 404;
    }

    # Deny sensitive project files by name, wherever they'd be served from
    location ~* ^/(composer\.(json|lock)|package(-lock)?\.json|artisan|phpunit\.xml.*|vite\.config\.js|.*\.(sql|log|bak|backup|swp|zip|tar|gz|sh|ini|ya?ml)|.*\.sqlite)$ {
        deny all;
        return 404;
    }

    # Never execute PHP inside the public storage/upload directory —
    # uploaded files must be served as static assets only (see Phase 4).
    location ^~ /storage/ {
        location ~ \.php$ { deny all; return 404; }
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
```

### 9e. Dependency audit (run 2026-07-07)

`composer audit` found 4 medium-severity advisories, all patched by bumping
patch/minor versions already allowed by `composer.json`'s existing
constraints (no breaking changes, `php artisan test` still green after):

| Package | Was | Now | Advisory |
|---|---|---|---|
| `laravel/framework` | v12.61.0 | v12.62.0 | Signed-URL path confusion (GHSA-crmm-hgp2-wgrp) |
| `guzzlehttp/guzzle` | 7.10.5 | 7.13.2 | Dot-only cookie domain matches all hosts; silent HTTPS→cleartext proxy downgrade |
| `guzzlehttp/psr7` | 2.10.4 | 2.12.3 | CRLF injection in HTTP start-line serialization |

`npm audit` reported 0 vulnerabilities — no action needed.

### 9f. Known secret exposure — action required

`menu-application.zip` in the project root contains a full copy of `.env`
(confirmed by inspecting the archive). Per this hardening plan's own
instructions, **treat every secret in that .env as leaked**: `APP_KEY`,
`REVERB_APP_SECRET`, `VAPID_PRIVATE_KEY`. This local `.env` is the dev/SQLite
one (`APP_ENV=local`), not necessarily production's — but if any of these
values were ever copied into the live `.env` on `menukosh.pinakra.com`,
rotate them there too (see the `APP_KEY ROTATION` section in
`.env.production.example`, and regenerate Reverb/VAPID credentials the same
way). Delete or move the zip out of the project directory once you've copied
whatever you needed from it — it is now `.gitignore`d so it won't be
committed if you `git init` this project, but it still sits on disk in
plaintext.
