Skip to content

Mobile LAN Setup

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.


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)”
Section titled “Option A — Caddy with mkcert (recommended for home server)”
Terminal window
# Install mkcert on the server
apt install libnss3-tools
curl -L https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-linux-amd64 -o /usr/local/bin/mkcert
chmod +x /usr/local/bin/mkcert
mkcert -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.pem

Caddyfile (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
}
}
Terminal window
systemctl restart caddy
Terminal window
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.

Terminal window
# Copy the mkcert root CA to a web server so you can download it on the phone
cp "$(mkcert -CAROOT)/rootCA.pem" /var/www/propria/api/public/rootCA.crt

Android:

  1. Open https://192.168.1.50/rootCA.crt on the phone browser.
  2. Settings → Security → Install certificate → CA certificate.

iOS:

  1. Open https://192.168.1.50/rootCA.crt — iOS downloads a profile.
  2. Settings → General → VPN & Device Management → Install.
  3. Settings → General → About → Certificate Trust Settings → Enable.

Terminal window
# From the project root
cd mobile
pnpm install
pnpm 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:

scripts/deploy-mobile.sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
(cd mobile && pnpm install --frozen-lockfile && pnpm build)
rm -rf api/public/mobile
cp -r mobile/dist api/public/mobile
echo "Mobile PWA deployed to api/public/mobile"

  1. Connect the phone to the same Wi-Fi network as the server.
  2. Open Chrome (Android) or Safari (iOS): https://192.168.1.50/mobile/
  3. Accept the certificate warning if using a self-signed cert.
  4. Tap Add to Home Screen / Install App.
  5. On the Setup screen, enter:
    • Server URL: https://192.168.1.50 (no /api suffix — the app appends it)
    • Email / Password: same credentials as the dashboard
  6. Tap Log in — you’re ready.

The goal: LAN devices can reach port 443; WAN cannot.

Terminal window
# Allow from LAN subnet only
ufw allow from 192.168.1.0/24 to any port 443
ufw allow from 192.168.1.0/24 to any port 8000
# Deny from everywhere else (default deny-incoming should cover this)
ufw reload

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.


Terminal window
# Terminal 1: Laravel
cd 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):

Terminal window
# Install cloudflared
# Then:
cloudflared tunnel --url http://localhost:5174 # point to Vite dev server

The tunnel URL is HTTPS. Install from it, test on phone, forget the URL.


ActionNetwork neededWhat happens
Open appNoLoads from Service Worker cache
Browse downloaded properties/leasesNoReads IndexedDB
Create / edit reportNoWrites to IndexedDB, marks dirty = true
Take photosNoBlobs stored in IndexedDB
Sync buttonYes — LANPOST /sync/push sends dirty reports + photos; updates IDs
Download for offlineYes — LANGET /sync/pull replaces local data for selected buildings

The Sync button is only enabled when GET /sync/health returns ok.


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).


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.25

Replace it everywhere with the client machine’s real LAN IP.

On the Windows machine, open PowerShell:

Terminal window
ipconfig

Find the active adapter’s IPv4 address, for example:

IPv4 Address . . . . . . . . . . : 192.168.1.25

For 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.

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/.

From the project root:

Terminal window
cd mobile
pnpm install
pnpm build

Copy the built files:

Terminal window
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.html

9.4. Install and configure Laravel dependencies

Section titled “9.4. Install and configure Laravel dependencies”

From C:\propria\api:

Terminal window
composer install
php artisan key:generate
php artisan migrate

Configure .env for the client’s database and local URL. Typical values:

APP_URL=https://192.168.1.25

If 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 modules

Enable:

ssl_module
rewrite_module
headers_module

If you choose the proxy variant later, also enable:

proxy_module
proxy_http_module

Restart all WAMP services after enabling modules.

Install mkcert using Chocolatey:

Terminal window
choco install mkcert

Or download mkcert.exe from:

https://github.com/FiloSottile/mkcert/releases

Put mkcert.exe somewhere in PATH, for example:

C:\Windows\System32\mkcert.exe

Install the local root CA on the Windows machine:

Terminal window
mkcert -install

Generate a certificate for the client LAN IP:

Terminal window
mkdir C:\certs
cd C:\certs
mkcert 192.168.1.25

This creates files similar to:

192.168.1.25.pem
192.168.1.25-key.pem

Apache will use these files for HTTPS.

Find the mkcert root CA:

Terminal window
mkcert -CAROOT

Copy this file:

rootCA.pem

Rename a copy to:

rootCA.crt

Transfer 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 certificate

iOS:

Settings → General → VPN & Device Management → Install profile
Settings → General → About → Certificate Trust Settings → Enable full trust

Without 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.conf
C:\wamp64\bin\apache\apache2.4.x\conf\httpd.conf

Example:

<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 from C:/propria/api/public/mobile/
  • https://192.168.1.25/api/... is handled by Laravel through public/index.php
  • both PWA and API share the same origin

Restart WAMP after saving the config.

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:

Terminal window
cd C:\propria\api
php artisan serve --host=127.0.0.1 --port=8000

Then 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.

Allow inbound HTTPS on the Windows machine:

Windows Defender Firewall → Advanced settings → Inbound Rules → New Rule

Rule:

Type: Port
TCP: 443
Allow connection
Profiles: Private
Name: Propria HTTPS LAN

Do 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.

Open on the Windows machine:

https://192.168.1.25/mobile/
https://192.168.1.25/api/sync/health

Expected API health response:

{"status":"ok","server_time":"..."}

If the browser shows a certificate warning, the mkcert root CA is not trusted on that machine.

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.25

Do not add /api and do not add :8000.

  • Rebuild the mobile app after code changes:
Terminal window
cd mobile
pnpm 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.