Installer
Updated Oct 19, 2026
Matterly ships a production web installer at /install. It runs only while the app is not locked.
Installation workflow
flowchart TD
A[Visit any URL] --> B{storage/installed?}
B -->|no| C[/install Welcome]
B -->|yes| D[Normal app]
C --> E[Requirements]
E --> F[Environment]
F --> G[Database]
G --> H[Test DB / Migrate / Seed]
H --> I[Super Admin]
I --> J[Write lock file]
J --> K[Finished → Login]
| Step | Route | What happens |
|---|---|---|
| 1 Welcome | GET /install |
Overview |
| 2 Requirements | GET /install/requirements |
PHP, extensions, permissions |
| 3 Environment | GET/POST /install/environment |
App name, URL, env, debug → session |
| 4 Database | GET/POST /install/database |
MySQL + install actions |
| 4b Test | POST /install/database/test |
Connection only |
| 5–7 Install | inside storeDatabase |
Key, migrate, seed, link, clear; .env persisted after response |
| 8 Admin | GET/POST /install/admin |
Super Admin user |
| 9 Lock | Installer::lock() |
storage/installed |
| 10 Done | GET /install/finished |
Auto-redirect to /login |
Requirements
Checked by InstallerService::requirements():
- PHP ≥ 8.2
- Extensions: BCMath, Ctype, JSON, Mbstring, OpenSSL, PDO, PDO MySQL, Tokenizer, cURL
- Writable:
storage/,bootstrap/cache/,.env(or project root)
Environment setup
Collected on step 3 and written into .env during install:
| Field | .env key |
|---|---|
| App name | APP_NAME |
| App URL | APP_URL |
| Environment | APP_ENV (production / staging / local) |
| Debug | APP_DEBUG |
Also forced for simple hosting:
SESSION_DRIVER=fileCACHE_STORE=fileQUEUE_CONNECTION=sync
Pre-boot: bootstrap/ensure-env.php copies .env.example and ensures a non-empty APP_KEY so the wizard can load.
Database configuration
| Field | .env key |
|---|---|
| Host | DB_HOST |
| Port | DB_PORT |
| Database | DB_DATABASE |
| Username | DB_USERNAME |
| Password | DB_PASSWORD |
| Connection | DB_CONNECTION=mysql |
Test connection uses a temporary install_test PDO connection (5s timeout).
Install database then:
- Applies runtime MySQL config
- Generates
APP_KEYin memory if missing php artisan migrate --forcedb:seed --force(demo) orPlanSeeder(clean)storage:link(best-effort)optimize:clear(best-effort)- Schedules atomic
.envwrite on request termination (avoidsphp artisan servemid-request restart /ERR_EMPTY_RESPONSE)
APP_KEY generation
- Prefer existing
config('app.key'). - Otherwise generate
base64:+ 32 random bytes and set runtime config. - Persist via
.envAPP_KEY=with other installer values (not viakey:generatemid-request).
Migrations & seeders
| Mode | Command |
|---|---|
| Demo | db:seed --force (full DatabaseSeeder) |
| Clean | db:seed --class=PlanSeeder --force |
Migrations create the full SaaS and practice management schema (firms, matters, invoices, AI, knowledge, etc.).
Admin creation
InstallerService::createSuperAdmin():
- Requires
userstable - Refuses if already installed
Userwithrole = super_admin,firm_id = null- Optional
ENVATO_PURCHASE_CODEin.env - Calls
Installer::lock()
Installation lock
| Item | Detail |
|---|---|
| Path | storage/installed |
| Created | After Super Admin step |
| Effect | RedirectIfInstalled blocks /install/*; EnsureInstalled allows normal app |
| Reset | Delete the file (and typically reset DB) to re-run wizard |
GET /install/finished remains reachable after lock to show success and redirect to login.
Security notes
- Wizard blocked after lock.
- Step middleware enforces order (environment before database, schema before admin).
- Install routes skip DB probing in
EnsureInstalledto avoid hangs on bad MySQL hosts. - Passwords validated (min 8, confirmed) via
StoreAdminRequest.
Key classes
| Class | Role |
|---|---|
InstallController |
HTTP steps |
InstallerService |
Business logic |
App\Support\Installer |
Lock + atomic writeEnv |
EnsureInstalled / RedirectIfInstalled / EnsureInstallStep |
Gates |
Store*Request |
Validation |