Mobile LAN Setup
Mobile PWA — LAN Setup Runbook
Section titled “Mobile PWA — LAN Setup Runbook”The mobile app (mobile/) is a PWA that runs offline on the phone and syncs to
the Laravel API only on the local network. The API is never exposed to the
public internet.
Architecture summary
Section titled “Architecture summary”Office / home Wi-Fi (private LAN — no WAN inbound)
[Phone] [Server / developer PC] ────────────────────── ───────────────────────── Chrome PWA Caddy / nginx (HTTPS :443) IndexedDB (data + photo blobs) └─ Laravel :8000 Service Worker (app shell) └─ MySQL api/public/mobile/ ← built PWA dist- The phone fetches the PWA shell once from
https://<LAN-IP>/mobile/. - After install the app runs fully offline — no network needed in the field.
- Sync (pull / push) only fires when the phone is connected to the same Wi-Fi.
- Photos are stored as blobs in IndexedDB until pushed.
1. HTTPS on LAN (required for PWA install)
Section titled “1. HTTPS on LAN (required for PWA install)”Option A — Caddy with mkcert (recommended for home server)
Section titled “Option A — Caddy with mkcert (recommended for home server)”# Install mkcert on the serverapt install libnss3-toolscurl -L https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-linux-amd64 -o /usr/local/bin/mkcertchmod +x /usr/local/bin/mkcertmkcert -install # installs a local CA
# Generate cert for your LAN IP (replace with your actual IP)mkcert 192.168.1.50 localhost 127.0.0.1 ::1# Produces: 192.168.1.50+3.pem 192.168.1.50+3-key.pemCaddyfile (single file, place at /etc/caddy/Caddyfile):
https://192.168.1.50 { tls /path/to/192.168.1.50+3.pem /path/to/192.168.1.50+3-key.pem
# Serve built PWA shell handle /mobile/* { root * /var/www/propria/api/public file_server }
# Proxy API to Laravel handle /api/* { reverse_proxy 127.0.0.1:8000 }
# Redirect root to dashboard (optional) handle { reverse_proxy 127.0.0.1:5173 }}systemctl restart caddyOption B — nginx + self-signed cert
Section titled “Option B — nginx + self-signed cert”openssl req -x509 -nodes -days 730 -newkey rsa:2048 \ -keyout /etc/ssl/private/lan.key \ -out /etc/ssl/certs/lan.crt \ -subj "/CN=192.168.1.50"/etc/nginx/sites-available/propria:
server { listen 443 ssl; server_name 192.168.1.50; ssl_certificate /etc/ssl/certs/lan.crt; ssl_certificate_key /etc/ssl/private/lan.key;
location /mobile/ { alias /var/www/propria/api/public/mobile/; try_files $uri $uri/ /mobile/index.html; }
location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }}2. Install the mkcert CA on each phone (one-time per phone)
Section titled “2. Install the mkcert CA on each phone (one-time per phone)”Skip if you used a self-signed cert — you will need to accept the cert warning on first visit instead.
# Copy the mkcert root CA to a web server so you can download it on the phonecp "$(mkcert -CAROOT)/rootCA.pem" /var/www/propria/api/public/rootCA.crtAndroid:
- Open
https://192.168.1.50/rootCA.crton the phone browser. - Settings → Security → Install certificate → CA certificate.
iOS:
- Open
https://192.168.1.50/rootCA.crt— iOS downloads a profile. - Settings → General → VPN & Device Management → Install.
- Settings → General → About → Certificate Trust Settings → Enable.
3. Build and deploy the mobile PWA
Section titled “3. Build and deploy the mobile PWA”# From the project rootcd mobilepnpm installpnpm build# Output: mobile/dist/
# Copy into Laravel public (served by Caddy/nginx above)cp -r mobile/dist/. api/public/mobile/Or add to your deployment script:
#!/usr/bin/env bashset -euo pipefailcd "$(dirname "$0")/.."(cd mobile && pnpm install --frozen-lockfile && pnpm build)rm -rf api/public/mobilecp -r mobile/dist api/public/mobileecho "Mobile PWA deployed to api/public/mobile"4. Install the PWA on the phone
Section titled “4. Install the PWA on the phone”- Connect the phone to the same Wi-Fi network as the server.
- Open Chrome (Android) or Safari (iOS):
https://192.168.1.50/mobile/ - Accept the certificate warning if using a self-signed cert.
- Tap Add to Home Screen / Install App.
- On the Setup screen, enter:
- Server URL:
https://192.168.1.50(no/apisuffix — the app appends it) - Email / Password: same credentials as the dashboard
- Server URL:
- Tap Log in — you’re ready.
5. Firewall rules (lock down WAN)
Section titled “5. Firewall rules (lock down WAN)”The goal: LAN devices can reach port 443; WAN cannot.
UFW (Ubuntu)
Section titled “UFW (Ubuntu)”# Allow from LAN subnet onlyufw allow from 192.168.1.0/24 to any port 443ufw allow from 192.168.1.0/24 to any port 8000# Deny from everywhere else (default deny-incoming should cover this)ufw reloadRouter
Section titled “Router”In your router’s port-forwarding / firewall rules, do not forward port 443 or 8000 to this machine. If the machine has a dynamic public IP, no action is needed — it won’t be reachable unless you explicitly forward.
6. Development workflow (test on phone)
Section titled “6. Development workflow (test on phone)”# Terminal 1: Laravelcd api && php artisan serve --host=0.0.0.0 --port=8000
# Terminal 2: Vite mobile (exposes on all interfaces)cd mobile && pnpm dev --host
# On phone browser: https://192.168.1.x:5173# (Vite dev mode serves over HTTP by default — for HTTPS use:)cd mobile && pnpm dev --host --https# Vite will generate a self-signed cert automatically.For camera + real PWA install testing during development, the easiest approach is a one-off Cloudflare Tunnel (no account required for temporary tunnels):
# Install cloudflared# Then:cloudflared tunnel --url http://localhost:5174 # point to Vite dev serverThe tunnel URL is HTTPS. Install from it, test on phone, forget the URL.
7. Sync behaviour reference
Section titled “7. Sync behaviour reference”| Action | Network needed | What happens |
|---|---|---|
| Open app | No | Loads from Service Worker cache |
| Browse downloaded properties/leases | No | Reads IndexedDB |
| Create / edit report | No | Writes to IndexedDB, marks dirty = true |
| Take photos | No | Blobs stored in IndexedDB |
| Sync button | Yes — LAN | POST /sync/push sends dirty reports + photos; updates IDs |
| Download for offline | Yes — LAN | GET /sync/pull replaces local data for selected buildings |
The Sync button is only enabled when GET /sync/health returns ok.
8. Multi-device note
Section titled “8. Multi-device note”The same Sanctum token is used for all API calls. The AuthService::login()
currently deletes all existing tokens on login. If the dashboard user and
the phone user are the same account, logging in on the phone will log out the
dashboard.
Mitigation (v1): use a dedicated agent account for the phone.
Future: adjust AuthService::login() to keep tokens with a device_name
column (Sanctum supports this natively).
Windows Set-up
Section titled “Windows Set-up”9. Windows client LAN deployment — WAMP + Apache + mkcert
Section titled “9. Windows client LAN deployment — WAMP + Apache + mkcert”Use this option when the client already has WAMP / Apache 2 on a Windows machine and you want Apache to do the same job as nginx:
- serve the built PWA at
https://CLIENT-IP/mobile/ - serve the Laravel API at
https://CLIENT-IP/api/ - terminate trusted HTTPS for PWA install/offline support
- keep the app on one origin to avoid CORS issues
- allow large photo sync uploads
This section assumes a client Windows LAN IP such as:
192.168.1.25Replace it everywhere with the client machine’s real LAN IP.
9.1. Choose fixed LAN IP
Section titled “9.1. Choose fixed LAN IP”On the Windows machine, open PowerShell:
ipconfigFind the active adapter’s IPv4 address, for example:
IPv4 Address . . . . . . . . . . : 192.168.1.25For production use, configure a DHCP reservation in the router so this machine always keeps the same IP. The PWA install URL and certificate are tied to this address.
9.2. Prepare project paths
Section titled “9.2. Prepare project paths”Example target path on Windows:
C:\propria\Recommended layout:
C:\propria\api\C:\propria\dashboard\C:\propria\mobile\The deployed PWA files must end up here:
C:\propria\api\public\mobile\Only the contents of mobile/dist/ go into api/public/mobile/; do not
copy the whole source mobile/ folder into public/mobile/.
9.3. Build and copy the PWA
Section titled “9.3. Build and copy the PWA”From the project root:
cd mobilepnpm installpnpm buildCopy the built files:
Remove-Item -Recurse -Force C:\propria\api\public\mobile\*Copy-Item -Recurse C:\propria\mobile\dist\* C:\propria\api\public\mobile\Check that this file exists:
C:\propria\api\public\mobile\index.html9.4. Install and configure Laravel dependencies
Section titled “9.4. Install and configure Laravel dependencies”From C:\propria\api:
composer installphp artisan key:generatephp artisan migrateConfigure .env for the client’s database and local URL. Typical values:
APP_URL=https://192.168.1.25If WAMP Apache runs Laravel directly, you do not need to keep
php artisan serve running.
9.5. Enable required Apache modules in WAMP
Section titled “9.5. Enable required Apache modules in WAMP”In the WAMP tray menu:
Wamp tray icon → Apache → Apache modulesEnable:
ssl_modulerewrite_moduleheaders_moduleIf you choose the proxy variant later, also enable:
proxy_moduleproxy_http_moduleRestart all WAMP services after enabling modules.
9.6. Install mkcert on Windows
Section titled “9.6. Install mkcert on Windows”Install mkcert using Chocolatey:
choco install mkcertOr download mkcert.exe from:
https://github.com/FiloSottile/mkcert/releasesPut mkcert.exe somewhere in PATH, for example:
C:\Windows\System32\mkcert.exeInstall the local root CA on the Windows machine:
mkcert -installGenerate a certificate for the client LAN IP:
mkdir C:\certscd C:\certsmkcert 192.168.1.25This creates files similar to:
192.168.1.25.pem192.168.1.25-key.pemApache will use these files for HTTPS.
9.7. Install the mkcert root CA on phones
Section titled “9.7. Install the mkcert root CA on phones”Find the mkcert root CA:
mkcert -CAROOTCopy this file:
rootCA.pemRename a copy to:
rootCA.crtTransfer rootCA.crt to each phone and install it as a trusted CA.
Android path varies by vendor, usually:
Settings → Security → Encryption & credentials → Install a certificate → CA certificateiOS:
Settings → General → VPN & Device Management → Install profileSettings → General → About → Certificate Trust Settings → Enable full trustWithout this step the site may load with a warning, but Chrome may only create a shortcut instead of installing a real PWA.
9.8. Apache HTTPS VirtualHost — direct Laravel via WAMP
Section titled “9.8. Apache HTTPS VirtualHost — direct Laravel via WAMP”This is the recommended WAMP setup. Apache serves Laravel directly from
api/public and Laravel’s .htaccess routes /api/* to index.php.
Add a vhost in WAMP’s Apache vhosts file, commonly one of:
C:\wamp64\bin\apache\apache2.4.x\conf\extra\httpd-vhosts.confC:\wamp64\bin\apache\apache2.4.x\conf\httpd.confExample:
<VirtualHost *:443> ServerName 192.168.1.25
SSLEngine on SSLCertificateFile "C:/certs/192.168.1.25.pem" SSLCertificateKeyFile "C:/certs/192.168.1.25-key.pem"
DocumentRoot "C:/propria/api/public"
# Allow large inventory photo syncs (100 MB) LimitRequestBody 104857600
<Directory "C:/propria/api/public"> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory>
ErrorLog "C:/wamp64/logs/propria-ssl-error.log" CustomLog "C:/wamp64/logs/propria-ssl-access.log" common</VirtualHost>Why this works:
https://192.168.1.25/mobile/serves files fromC:/propria/api/public/mobile/https://192.168.1.25/api/...is handled by Laravel throughpublic/index.php- both PWA and API share the same origin
Restart WAMP after saving the config.
9.9. Optional HTTP redirect to HTTPS
Section titled “9.9. Optional HTTP redirect to HTTPS”Add an HTTP vhost to redirect users:
<VirtualHost *:80> ServerName 192.168.1.25 Redirect permanent / https://192.168.1.25/</VirtualHost>This makes accidental http://192.168.1.25/mobile/ requests go to HTTPS.
9.10. If using Apache as reverse proxy instead
Section titled “9.10. If using Apache as reverse proxy instead”Use this variant only if you want to keep Laravel running with:
cd C:\propria\apiphp artisan serve --host=127.0.0.1 --port=8000Then Apache proxies /api/ to the artisan server:
<VirtualHost *:443> ServerName 192.168.1.25
SSLEngine on SSLCertificateFile "C:/certs/192.168.1.25.pem" SSLCertificateKeyFile "C:/certs/192.168.1.25-key.pem"
DocumentRoot "C:/propria/api/public" LimitRequestBody 104857600
<Directory "C:/propria/api/public"> Options Indexes FollowSymLinks AllowOverride All Require all granted </Directory>
ProxyPreserveHost On RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Port "443"
ProxyPass /api/ http://127.0.0.1:8000/api/ ProxyPassReverse /api/ http://127.0.0.1:8000/api/
ErrorLog "C:/wamp64/logs/propria-ssl-error.log" CustomLog "C:/wamp64/logs/propria-ssl-access.log" common</VirtualHost>For a client installation, the direct WAMP/Laravel setup in 9.8 is usually
cleaner because it does not require a terminal process running artisan serve.
9.11. Windows firewall
Section titled “9.11. Windows firewall”Allow inbound HTTPS on the Windows machine:
Windows Defender Firewall → Advanced settings → Inbound Rules → New RuleRule:
Type: PortTCP: 443Allow connectionProfiles: PrivateName: Propria HTTPS LANDo not expose this port through the internet router unless the client explicitly wants public access. For the LAN-only architecture, no router port forwarding is needed.
9.12. Test from Windows
Section titled “9.12. Test from Windows”Open on the Windows machine:
https://192.168.1.25/mobile/https://192.168.1.25/api/sync/healthExpected API health response:
{"status":"ok","server_time":"..."}If the browser shows a certificate warning, the mkcert root CA is not trusted on that machine.
9.13. Install the PWA on the phone
Section titled “9.13. Install the PWA on the phone”Connect the phone to the same Wi-Fi as the Windows machine and open:
https://192.168.1.25/mobile/Chrome should show Install app. If it only shows Add to Home screen, the certificate is probably not trusted on the phone.
In the PWA login screen:
Server URL = https://192.168.1.25Do not add /api and do not add :8000.
9.14. Operational notes
Section titled “9.14. Operational notes”- Rebuild the mobile app after code changes:
cd mobilepnpm build- Copy
mobile/dist/*again into:
C:\propria\api\public\mobile\- After each deployment, open the PWA once while online so the service worker updates its cache.
- Keep the Windows machine powered on when users need to sync.
- Use a DHCP reservation so the IP does not change.