# Irestora — FrancoPOS V2 Multi-Store: Hosting & Installation

Production-ready install notes for the POS: a dependency-free PHP 8 + SQLite single-page app.
Document root is `pos/public`; a front controller (`pos/router.php`) serves the SPA and routes
`/api/*` to the JSON API.

## 1. Requirements

- **PHP 8.0+** (developed on 8.2) with extensions:
  - `pdo_sqlite` (database) — **required**
  - `json`, `session`, `openssl` (usually enabled by default)
  - No Composer, no npm build, no Node runtime. No external packages.
- **Filesystem**: the SQLite DB is auto-created + migrated on first request if missing; the web
  server user needs read/write on `pos/data/` (and its parent dirs).

## 2. Repository layout (what is on the host)

```
irestora/
├── pos/
│   ├── router.php          # front controller (dev + prod)
│   ├── public/             # WEBROOT — the only directory that should be public
│   │   ├── index.html
│   │   └── css/, js/       # SPA assets (served statically)
│   ├── lib/                # app + API code (kept OUT of webroot)
│   ├── data/               # SQLite DB: pos.sqlite (+ -wal, -shm). Must be writable.
│   ├── run.ps1             # dev-only helper (Windows)
│   └── seed.php            # `php pos/lib/seed.php` demo-data seeder
└── smoke/                  # CDP smoke suite (dev/test only, not deployed)
```

The router serves any real file inside `public/` directly, hands `/api/<route>` to
`lib/api/index.php`, and sends everything else to `index.html` (SPA fallback).

## 3. Development install

```bash
cd irestora
php -S 0.0.0.0:8080 -t pos/public pos/router.php
```

DB at `pos/data/pos.sqlite` is created + schema-migrated on the first request. To load demo
data, run `php pos/lib/seed.php` (idempotent — it exits early when seed data already exists; to
re-seed from scratch delete `pos/data/pos.sqlite*` first, then seed, then restart the server).

### 3.1 Automated installers

- **`php install.php`** — cross-platform CLI installer (checks PHP version/extensions, verifies
  the layout and writable `pos/data`, creates/migrates the DB). Flags:
  `--seed` (demo data), `--admin-email=.. --admin-pass=..` (create/update an admin login),
  `--serve=8080` (spawn `php -S`, probe `/api/me`, then stop), `--json`. Safe to re-run;
  `php install.php --seed --admin-email=you@x.io --admin-pass='…'` is a full unattended setup.
- **`.\install.ps1`** — Windows one-shot (owns the dev server): checks PHP, initializes + seeds
  the DB via `install.php`, starts the dev server if not already listening, opens the browser,
  and with `-Test` runs the CDP smoke suite. `-Rebuild` stops the server and rebuilds the DB
  from scratch; `-Port 9090 -NoBrowser -NoSeed` for non-standard runs.

## 4. Production install

### 4.1 PHP-FPM / php.ini notes

- `memory_limit >= 128M`, `upload_max_filesize/post_max_size` as needed.
- `session.cookie_httponly = On`; behind HTTPS also `session.cookie_secure = On`.
- Production: `display_errors = Off`, `log_errors = On` (the API already returns a generic
  `{"error":"server.error"}` with the message in `detail` for debugging).
- Timezone is forced to `UTC` in `bootstrap.php`; report/export `date(...)` filters are
  date-based and perform the same, so no TZ config is required.

### 4.2 Apache (mod_rewrite + PHP-FPM/CGI)

Set `DocumentRoot` to `pos/public` and let Apache reach `router.php` one level up:

```apache
<VirtualHost *:80>
    ServerName pos.example.com
    DocumentRoot /srv/irestora/pos/public

    <Directory /srv/irestora/pos/public>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>
```

`pos/public/.htaccess`:

```apache
RewriteEngine On

# Serve real files (css/js/images) directly
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# Everything else -> front controller one level above docroot
RewriteRule ^ ../router.php [L]
```

Note: the `../` rewrite works because the vhost owns both directories; if `AllowOverride` is
disabled, put the same rules in the vhost `<Directory>` block instead. Apache must have
execute permission on `pos/router.php` (typically satisfied for same-filesystem vhosts).

### 4.3 Nginx (recommended)

Simplest safe layout: keep `root` at `pos` and explicitly deny the private directories, then
send all misses to `router.php`:

```nginx
server {
    listen 80;
    server_name pos.example.com;
    root /srv/irestora/pos;

    # Never expose app code or the database (seed.php and run.ps1 live under lib/ anyway)
    location ^~ /lib/ { deny all; }
    location ^~ /data/ { deny all; }
    location ~ \.(ps1|md)$ { deny all; }

    # Real static assets inside public/ are served as files;
    # everything else goes through the front controller.
    location / {
        try_files $uri /router.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root/router.php;
        fastcgi_pass unix:/run/php/php-fpm.sock;   # adjust to your pool
    }
}
```

(Alternative harder-lockdown layout: `root pos/public` + a `@app` named location that
`fastcgi_param`s `SCRIPT_FILENAME` to `/srv/irestora/pos/router.php`. The config above is the
recommended one: it keeps `lib`/`data` denied by rule rather than by docroot boundary.)

Do **not** use `php -S` in production.

### 4.4 File permissions

```bash
chown -R www-data:www-data /srv/irestora/pos/data   # web user must write the DB
chmod 750 /srv/irestora/pos/data
chmod -R u+rw /srv/irestora/pos/data
```

`pos/public` may be read-only. Keep `pos/lib`, `pos/router.php`, `seed.php` readable by the
web user (the FPM worker must execute them) but out of the public reach.

### 4.5 HTTPS

Put a TLS terminator in front (Let's Encrypt / certbot, or a reverse proxy). Enable
`session.cookie_secure = On` and (recommended) a `session.cookie_samesite = Lax`. The API sends
no CORS headers — it is intentionally same-origin only, so do not add CORS middleware.

## 5. Database init, seeding, and first login

1. Start the PHP-FPM site. On the first request the schema is created automatically
   (`Database::__construct` migrations in `lib/db.php`, including `has_column` ALTERs).
2. (Optional) Seed demo data so the app is immediately usable:
   ```bash
   php pos/lib/seed.php
   ```
   This is **idempotent**: it returns early once seeded. To reset:
   ```bash
   # stop the app / pause writes, then:
   rm -f pos/data/pos.sqlite pos/data/pos.sqlite-wal pos/data/pos.sqlite-shm
   php pos/lib/seed.php
   ```
3. Log in with the seeded credentials and change them immediately:
   - `admin@demo.local` / `admin123` (role admin)
   - `cashier@demo.local` / `cash123`, `kitchen@demo.local` / `kitchen123` — or delete them.
   Passwords are `password_hash()` (bcrypt) hashed.

## 6. Backups (SQLite + WAL)

The DB runs in WAL mode (`PRAGMA journal_mode = WAL`), so **never** copy the `-wal`/`-shm`
files or the DB mid-write as a "backup". Use the SQLite online backup:

```bash
sqlite3 pos/data/pos.sqlite ".backup '/backups/pos-2026-09-27.db'"
# or, from PHP:
# $pdo->exec("VACUUM INTO '/backups/pos-2026-09-27.db'");
```

Restore: stop the app, replace `pos.sqlite`, delete stale `-wal`/`-shm`, start again. Schedule
nightly backups (cron) before the upload of fresh static assets; the DB is `data/`-only — no
other state exists, so restores are a single-file swap.

## 7. Upgrades

The app has no Composer lockfile to update. To upgrade:

1. Stop the app.
2. Back up `pos/data/pos.sqlite` (see §6).
3. Replace the code files (keep `pos/data`).
4. Start the app — `ensure_schema()` runs the ALTER migrations (`has_column`) on boot, adding
   new columns (station, delivery_charge, etc.) to your existing DB.
5. Re-run `smoke/test.ps1` in an isolated dev copy as a check, or spot-check the affected views.

## 8. Troubleshooting

| Symptom | Cause / fix |
| --- | --- |
| `500 server.error` + `detail: SQLSTATE[HY000] …locked` | DB locked/dir not writable. Fix §4.4 permissions, stop any other process holding the `.sqlite` (e.g. the dev `php -S` server), then retry. |
| Blank page / `404` on `/api/<route>` | You must use `/api/<route>` exactly — the router does **not** accept `/api/index.php?route=`. |
| New columns missing after upgrade | DB not re-owned OR old server still running. Restart PHP-FPM and confirm `pos/data/*` perms. |
| Seeder does nothing | Expected — it is idempotent. Delete the DB first for a full re-seed (§5). |
| `openssl already loaded` warning on CLI | Harmless config warning; unrelated to the app. |
| Sessions/logins drop | Confirm `session.save_path` is writable and the cookie domain matches the vhost hostname. |

## 9. Security checklist (before go-live)

- [ ] Document root is `pos/public` (Apache) or `lib/`+`data/` are denied (Nginx).
- [ ] Demo accounts removed or all passwords changed (no `admin123` in the wild).
- [ ] HTTPS enforced; `session.cookie_secure = On`; `session.cookie_httponly = On`.
- [ ] `display_errors = Off` in production php.ini.
- [ ] Backups scheduled (SQLite `.backup`), restore tested.
- [ ] No CORS/external hosts configured (API is same-origin by design).