# Installation

Source: https://docs.externa.qiick.io/docs/installation

> **Interactive installer**
> **Human happy path:** `composer create-project` → `php artisan externa:install` → run app. The wizard walks APP_NAME, APP_URL, DB profile, minimal vs full stack, optional seed / LOCAL_AI_URL, then migrate + npm build. Create-project itself only copies `.env` (if missing) and runs `key:generate` — **no** migrate/seed/SQLite assumption.

> **CI / non-interactive**
> `composer setup` remains the scripted path (install → `.env` → key → migrate → npm). Distinct from the human create-project handoff. Details: [Packagist & create-project](/docs/packagist#ci-non-interactive-not-the-human-path).

> **Faster path**
> **Operators:** [Packagist `create-project`](/docs/packagist) then `php artisan externa:install`. Copy-paste Herd `.env` without the wizard: [Quick Start](/docs/quick-start). This page is the fuller install reference.

This guide gets **externa-core** running on your machine: PHP dependencies, database of your choice, frontend build, seeded roles and super admin, and the concurrent dev processes (HTTP, queue, logs, Vite).

## Requirements

> Externa requires PHP **`^8.4.27`** from externa-core **1.2.2** ([`composer.json`](https://github.com/qiick-io/externa-core/blob/main/composer.json), [#248](https://github.com/qiick-io/externa-core/pull/248)). After the 21 Oct 2026 PHP emergency releases ([GHSA-w8hw-w9fx-j79w](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w), [GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)), the minimum safe patch versions are **8.4.27** or **8.5.12**. Upgrading is the only full mitigation. After switching Herd / system PHP, **restart** the PHP service so FPM loads the patched binary. Full notice: [externa-core discussion #247](https://github.com/qiick-io/externa-core/discussions/247). Hardening (keep after patch): [Deployment — PHP runtime hardening](/docs/deployment#php-runtime-hardening).

| Requirement                         | Notes                                                                                                                                           |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| PHP **8.4.27+** (or **8.5.12+**)    | Matches `composer.json` (`^8.4.27` from 1.2.2) and the repo `.php-version` pin. See security callout above for CVE context and the full notice. |
| Composer 2                          | Used for `composer setup` and `composer run dev`.                                                                                               |
| Node.js + npm                       | Frontend via Vite; Node 24 is pinned in `.nvmrc` when present.                                                                                  |
| SQLite (default)                    | Or [MySQL / MariaDB / Postgres](/docs/supported-databases) via `DB_*` after copying `.env`.                                                     |
| Laravel Herd (recommended on macOS) | PHP, nginx, `.test` hosts; Pro adds shared Reverb on `:8080`.                                                                                   |
| **Docker** (optional)               | Official Compose full stack — see [Docker local](#docker-compose-local-full-stack). Sail remains `require-dev` only.                            |

Optional for AI features:

- An OpenAI-compatible local gateway (e.g. [LM Studio](https://lmstudio.ai)) listening where `LOCAL_AI_URL` points (default `http://127.0.0.1:1234/v1`).
- A model that supports tool calling reliably — tiny models often hallucinate successful tool results.

> **Note**
> You need a **queue worker** for zip downloads, larger/bulk file duplicates, and collection imports. The `composer run dev` script starts `php artisan queue:listen` for you. If you only run `php artisan serve`, start a worker in a second terminal.

## Get the code

### Operators (Packagist)

```bash
composer create-project qiick/externa-core my-externa
cd my-externa
nvm use
php artisan externa:install
```

Details: [Packagist & create-project](/docs/packagist). Skip to [Start the development stack](#start-the-development-stack) when the installer already migrated, seeded, and built.

> **After install — staying current**
> `create-project` is **install only**. Pushing the tree to your own git remote does **not** auto-pull Externa updates. Recommended: track release tags or add `upstream` and merge `vX.Y.Z` (or `php artisan externa:upgrade --sync-upstream=vX.Y.Z`), then run `php artisan externa:upgrade` (migrate / permissions / cache). Full checklist: [Upgrade](/docs/upgrade).

### Contributors (git clone)

```bash
git clone https://github.com/qiick-io/externa-core.git
cd externa-core
nvm use
```

## Recommended: Laravel Herd

1. Install [Laravel Herd](https://herd.laravel.com) and ensure the CLI PHP version is **8.4.27+** (or **8.5.12+**).
2. Place the project (create-project or clone) in a directory Herd serves (or use `herd link` / park).
3. After changing the Herd PHP version, restart Herd’s PHP / FPM service, then confirm `php -v` and a request that hits FPM (not only CLI).
4. Open a terminal in the project root for the steps below.

Herd is optional — any local PHP 8.4.27+ + Composer + Node toolchain works. Prefer **Herd** for macOS `.test` hosts, or **Docker Compose** for a full container stack — pick one path per working tree ([Herd vs Docker](#choose-a-local-path)). Production php.ini notes (upload progress Off, body limits, no `PHP_VALUE` via nginx): [Deployment — PHP runtime hardening](/docs/deployment#php-runtime-hardening).

## Step-by-step install

### Interactive installer (`externa:install`)

Preferred after create-project (or on a fresh clone):

```bash
php artisan externa:install
```

| Prompt                    | Effect                                                                                                                                                                          |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| APP_NAME / APP_URL        | Written to `.env`                                                                                                                                                               |
| Database                  | `sqlite` (creates `database/database.sqlite` if missing), `pgsql`, `mysql`, or `mariadb`                                                                                        |
| Queue / broadcast profile | **Minimal** (database queue/cache/session, log broadcast — **no Redis**) or **Full** (redis queue/cache, reverb — needs Redis)                                                  |
| Seed                      | Optional `db:seed` (roles + super admin)                                                                                                                                        |
| LOCAL_AI_URL              | Optional; blank skips                                                                                                                                                           |
| Then                      | Preserve a valid existing `APP_KEY` or generate one, apply the selected stack to runtime config, `migrate`, `storage:link`, `npm install` / `npm run build` (unless `--no-npm`) |

Flags: `--force` (allow production-looking env / overwrite `.env` without an extra confirm when re-running), `--no-npm`, `--no-seed`. Without `--force`, re-run asks before overwriting an existing `.env` (decline = leave env untouched) and refuses a production-looking `.env`. From **1.2.2** ([#249](https://github.com/qiick-io/externa-core/pull/249)): the installer keeps or generates `APP_KEY` after rewriting `.env`, and the minimal profile migrates without Redis.

### Manual / CI: Composer setup

The `setup` script in `composer.json` is the **non-interactive** bootstrap (CI, scripted environments):

```bash
composer setup
```

That runs, in order:

1. `composer install`
2. Copy `.env.example` → `.env` if `.env` is missing
3. `php artisan key:generate`
4. `php artisan migrate --force`
5. `npm install`
6. `npm run build`

> **Note**
> `composer setup` **does** migrate against whatever `DB_*` is in `.env`. For SQLite, create the file first if missing: `touch database/database.sqlite`. Human operators should prefer `externa:install` after create-project instead.

```bash
# Only if using SQLite and the file is missing (composer setup / manual migrate)
touch database/database.sqlite
```

### Review environment defaults

Open `.env` and confirm at least:

```env
APP_URL=http://localhost
DB_CONNECTION=sqlite
SESSION_DRIVER=database
QUEUE_CONNECTION=database
CACHE_STORE=database
```

For Herd with a custom host, set `APP_URL` to that URL (including scheme). **HTTP is fine** for the rest of Externa (password login, TOTP, CMS, and **password** confirm on `/user/confirm-password`). Passkeys (sign-in, register, **Confirm with passkey**) need a secure context — `http://externa-core.test` hides those CTAs (browser rule). Local passkey testing: `herd secure …`, `APP_URL=https://…`, and Vite `detectTls` for the site host — [Passkeys — Local HTTPS](/docs/passkeys#local-https-herd-valet-for-passkey-testing); confirm-password details: [Confirm password with a passkey](/docs/passkeys#confirm-password-with-a-passkey). Database matrix: [Supported databases](/docs/supported-databases). File and AI tunables: [Environment variables](/docs/environment-variables).

### Link public storage

Required for publicly served uploads and related assets:

```bash
php artisan storage:link
```

### Seed roles, permissions, and super admin

```bash
php artisan db:seed
```

`DatabaseSeeder` calls:

1. `PermissionSeeder` — permissions from `App\Enums\PermissionEnum` via `permissions:sync`
2. `RoleSeeder` — roles `super-admin`, `admin`, `reader`, and system role `public`
3. `CreateSuperAdminSeeder` — first login user from `config/super_admin.php`

Override credentials **before** seeding if needed:

```env
INITIAL_SUPER_ADMIN_FIRST_NAME=Super
INITIAL_SUPER_ADMIN_LAST_NAME=Admin
INITIAL_SUPER_ADMIN_EMAIL=superadmin@example.com
INITIAL_SUPER_ADMIN_PASSWORD=password
```

Defaults when unset: email `superadmin@example.com`, password `password`.

> Seeding is meant for local (or carefully controlled) environments. Re-running seeders may conflict with existing users or permission rows depending on seeder idempotency. Prefer fresh databases for demos.

### Start the development stack

```bash
composer run dev
```

This uses `concurrently` to run:

| Process | Command                                          |
| ------- | ------------------------------------------------ |
| server  | `php artisan serve`                              |
| queue   | `php artisan queue:listen --tries=1 --timeout=0` |
| logs    | `php artisan pail --timeout=0`                   |
| vite    | `npm run dev`                                    |

Open the URL shown by `artisan serve` (or your Herd host). Unauthenticated visits to `/` redirect to login; authenticated users go to the dashboard.

### Full stack (Herd Redis + Reverb + Pulse)

When Redis and Herd Pro Reverb are enabled (see [Minimal vs full stack](/docs/minimal-vs-full-stack)):

1. Copy `BROADCAST_*` / `REVERB_*` / `VITE_REVERB_*` / Pulse keys from `.env.example` (Herd defaults on `:8080`).
2. Restart Vite after any `VITE_REVERB_*` change.
3. Run the two long-lived workers (Herd Reverb is already up — do not also `reverb:start`):

```bash
php84 artisan horizon
php84 artisan pulse:work
```

Dashboard **Health** then shows native Pulse/Horizon cards. Echo powers notifications, presence, and the avatar connection dot ([Reverb & Echo](/docs/reverb-and-echo)).

## Dev Container (VS Code / Codespaces)

Third official local path alongside Herd and Docker Compose:

1. Open externa-core in VS Code or Codespaces.
2. **Reopen in Container** (uses `.devcontainer/devcontainer.json` + root `compose.yaml`, service `app`).
3. Wait for post-create (`composer install`, `npm ci`, key, migrate, seed).
4. Browse forwarded port **8000** (Compose app publish).

Does not replace Herd or bare Compose — pick one path per working tree.

## Docker Compose (local full stack)

Official container path is **Compose** (`compose.yaml` in externa-core) — **not Sail**. Sail stays `require-dev` only; do not use it for day-to-day local or production.

### Choose a local path

| Path               | When                                                                                                | How                                                                       |
| ------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Herd** (macOS)   | Native PHP/nginx + optional Pro Redis/Reverb                                                        | [Recommended: Laravel Herd](#recommended-laravel-herd) + `composer setup` |
| **Bare metal**     | Any OS with PHP 8.4 + Composer + Node                                                               | Same as Herd steps without Herd hosts                                     |
| **Docker Compose** | Full stack in containers (Postgres + Redis + Horizon + Reverb + scheduler + Pulse + Mailpit + Vite) | This section                                                              |

Do **not** mix Herd-served HTTP with Compose `app` on the same checkout unless you know what you are doing. Prefer one path per working tree. Compose can share a host `.env` safely because containers override DB hosts / `APP_URL` via `COMPOSE_*` (see below).

### Quick start

```bash
cd /path/to/externa-core
cp .env.docker.example .env
docker compose up --build
```

Open **`http://localhost:8000`** (host port **`8000` → container `:80`** — not `:80` on the host). Cold first boot can take several minutes (`composer install` into the named `vendor` volume, then migrate).

### Default services and ports

| Service     | Role                                     | Host ports (defaults)                |
| ----------- | ---------------------------------------- | ------------------------------------ |
| `app`       | nginx + php-fpm (`development` target)   | **`8000`→80** (`APP_PORT`)           |
| `vite`      | Vite HMR (PHP+Node image)                | `5173` (`VITE_PORT`)                 |
| `pgsql`     | PostgreSQL 16 (default DB)               | `5432`                               |
| `redis`     | Queue / cache / session / Pulse / Reverb | `6379`                               |
| `horizon`   | `artisan horizon`                        | —                                    |
| `reverb`    | WebSockets                               | **`8081`→8080** (`REVERB_HOST_PORT`) |
| `scheduler` | `schedule:work`                          | —                                    |
| `pulse`     | `pulse:work`                             | —                                    |
| `mailpit`   | Local mail UI + SMTP                     | UI `8025` / SMTP `1025`              |
| `minio`     | Optional S3 (`--profile minio`)          | API `9000` / console `9001`          |
| `mysql`     | Optional (`--profile mysql` + overlay)   | `3306`                               |
| `mariadb`   | Optional (`--profile mariadb` + overlay) | **`3307`→3306** (Herd-like)          |

> **Ports vs Herd**
> **App:** Compose publishes HTTP on host **`:8000`**, not `:80`. **Reverb:** host **`:8081`** (container `8080`) so it does not clash with Herd Pro on **`:8080`**. `.env.docker.example` already sets `REVERB_PORT` / `VITE_REVERB_PORT=8081`.

### Env: COMPOSE overrides (Herd-safe)

Compose loads `.env` via `env_file`, then **overrides** critical keys in `environment:`:

| Knob                                                                  | Default                          | Purpose                                                                                                              |
| --------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `COMPOSE_APP_URL`                                                     | `http://localhost:8000`          | Forces container `APP_URL` so session/CSRF match the published URL even if host `.env` still has a Herd `*.test` URL |
| `COMPOSE_DB_CONNECTION`                                               | `pgsql`                          | DB driver inside containers                                                                                          |
| `COMPOSE_DB_HOST`                                                     | `pgsql`                          | Service DNS name (never `127.0.0.1` from a Herd `.env`)                                                              |
| `COMPOSE_DB_PORT`                                                     | `5432`                           | Port **inside** the network                                                                                          |
| `COMPOSE_DB_DATABASE` / `COMPOSE_DB_USERNAME` / `COMPOSE_DB_PASSWORD` | `externa` / `externa` / `secret` | Shared by app services and the DB container                                                                          |

Keep host Herd `.env` values like `DB_HOST=127.0.0.1` if you still run PHP on the host sometimes — Compose environment wins **inside** containers. Prefer starting from `.env.docker.example` for a Docker-only tree.

Also set (or keep from the sample):

```env
APP_PORT=8000
REVERB_HOST_PORT=8081
VITE_PORT=5173
RUN_MIGRATIONS=true
```

### Profiles and DB overlays

**MinIO (S3):**

```bash
docker compose --profile minio up --build
```

Then set `FILES_DISK=s3`, `AWS_ENDPOINT=http://minio:9000`, `AWS_USE_PATH_STYLE_ENDPOINT=true`, and matching keys — [Files configuration](/docs/files-configuration). `minio-init` creates the bucket.

**MySQL 8** (stops default Postgres via overlay — puts `pgsql` behind a non-default profile):

```bash
docker compose --profile mysql -f compose.yaml -f compose.mysql.yaml up --build
```

**MariaDB** (host **`:3307`** like Herd):

```bash
docker compose --profile mariadb -f compose.yaml -f compose.mariadb.yaml up --build
```

If an old Postgres container still owns host `:5432`:

```bash
docker compose stop pgsql
```

Database matrix: [Supported databases](/docs/supported-databases).

### Vite service and Wayfinder

The `vite` service builds from Dockerfile target **`vite`**: **PHP + Node** in one image. Wayfinder (and related Artisan) need PHP available while `npm run dev` runs. Vite waits until `app` is **healthy** (vendor volume ready). Do not replace this with a Node-only image.

### First boot: migrate, seed, login

1. `RUN_MIGRATIONS=true` (default) runs `php artisan migrate --force` in the `app` entrypoint after DB is reachable.
2. **Seed is not automatic.** After the stack is healthy:

```bash
docker compose exec app php artisan db:seed --force
```

3. Open `http://localhost:8000` and sign in with the seeded super admin (`config/super_admin.php`):

|          | Default                  |
| -------- | ------------------------ |
| Email    | `superadmin@example.com` |
| Password | `password`               |

Override with `INITIAL_SUPER_ADMIN_EMAIL` / `INITIAL_SUPER_ADMIN_PASSWORD` **before** seeding. Local/dev only.

Entrypoint also generates `APP_KEY` when empty, waits for DB, and runs `storage:link`. Sibling workers (`horizon`, `reverb`, …) wait for `vendor/autoload.php` so parallel `composer install` cannot corrupt the vendor volume.

### Health probes (local)

Compose `app` healthcheck:

```yaml
healthcheck:
  test: ['CMD-SHELL', 'curl -fsS http://127.0.0.1/health/ready || exit 1']
  interval: 10s
  timeout: 5s
  retries: 12
  start_period: 300s # cold vendor install can be long
```

| Endpoint            | Meaning                                           |
| ------------------- | ------------------------------------------------- |
| `GET /health/live`  | Process up                                        |
| `GET /health/ready` | DB (+ Redis when required) — **503** if not ready |
| `GET /up`           | Laravel framework liveness                        |

Details: [Deployment — Health probes](/docs/deployment#health-probes) · [Operations](/docs/operations).

### Troubleshooting

| Symptom                                                        | Likely cause                                               | Fix                                                                                                                                                                                      |
| -------------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Browser **connection refused** on `:8000` during first minutes | Old images without early nginx bind; or port not published | Pull latest `docker/entrypoint.sh` (binds `:80` early so TCP accepts while composer/migrate run — mid-boot may be **502**, not refused). Confirm `APP_PORT=8000` and `docker compose ps` |
| Session / CSRF / redirects to wrong host                       | Host Herd `APP_URL` leaking into containers                | Set `COMPOSE_APP_URL=http://localhost:8000` (Compose already overrides `APP_URL`)                                                                                                        |
| Echo / Reverb fails from browser                               | Port mismatch (Herd `:8080` vs Compose `:8081`)            | Use `VITE_REVERB_PORT=8081`; restart `vite` after env changes                                                                                                                            |
| Blank UI / missing assets                                      | `vite` stopped or never healthy                            | `docker compose logs vite`; ensure `app` became healthy first                                                                                                                            |
| DB connection errors inside app                                | `DB_HOST=127.0.0.1` from Herd `.env`                       | Rely on `COMPOSE_DB_*` / service names (`pgsql`, `mysql`, `mariadb`)                                                                                                                     |
| MySQL/MariaDB profile still starts Postgres                    | Overlay not applied                                        | Pass both `-f compose.yaml -f compose.mysql.yaml` (or `.mariadb.yaml`) **and** `--profile mysql` / `mariadb`                                                                             |

Useful:

```bash
docker compose ps
docker compose logs -f app
curl -fsS http://localhost:8000/health/ready
```

Production image + Compose: [Deploy with Docker](/docs/deploy-with-docker) · [Deployment — Docker](/docs/deployment#docker-production). Samples: `.env.docker.example`, `.env.docker.quick.example`, `.env.docker.prod.example`. Core README Docker section mirrors this.

### Alternative: SSR-oriented dev

If you need the Inertia SSR path:

```bash
composer run dev:ssr
```

That builds SSR assets and runs serve, queue, pail, and `inertia:start-ssr` instead of the Vite HMR process used by `composer run dev`.

## First login

1. Go to the login page (Fortify).
2. Sign in with the seeded super admin (default `superadmin@example.com` / `password`).
3. Confirm you can open **Dashboard**, **Users**, **Collections**, and **Files** according to `super-admin` permissions.

Built-in roles after seed:

| Role          | Access summary                                                                                                    |
| ------------- | ----------------------------------------------------------------------------------------------------------------- |
| `super-admin` | Every permission                                                                                                  |
| `admin`       | Every permission                                                                                                  |
| `reader`      | Read/show permissions only (`can-show-*`)                                                                         |
| `public`      | System / not assignable; no Spatie admin permissions — Public CMS API actor (collection/file matrices in role UI) |

Create additional users from the admin UI or tinker; assign roles and/or groups so [effective permissions](/docs/permissions) resolve as expected.

## Queue worker (required for background work)

With `QUEUE_CONNECTION=database`, jobs are stored in the `jobs` table. Keep a worker running whenever you use:

- Multi-file / folder **zip** preparation (`PrepareFilesZipJob`)
- **Duplicate** of folders, bulk selections, or files above `FILES_DUPLICATE_SYNC_MAX_BYTES` (`DuplicateFilesJob`)
- Collection **imports** (`ImportCollectionJob`)

`composer run dev` already includes `queue:listen`. Standalone:

```bash
php artisan queue:listen --tries=1 --timeout=0
```

> If zips stay “preparing”, imports never finish, or large duplicates hang, the usual cause is no worker (or a worker pointed at a different `.env` / database).

## Optional: local AI gateway

AI chat works without a remote cloud key when the **local** provider is configured (default).

1. Start LM Studio, vLLM, or another OpenAI-compatible server.
2. Align `.env` with the gateway:

```env
AI_DEFAULT_PROVIDER=local
LOCAL_AI_URL=http://127.0.0.1:1234/v1
LOCAL_AI_API_KEY=
LOCAL_AI_MODEL=openai/gpt-oss-20b
```

3. Prefer a model with reliable **tool calling**. The `.env.example` comment warns that tiny models often hallucinate success.
4. Optional hardening / features:

```env
AI_REMOTE_IMPORT_HOSTS=
AI_WEBHOOK_TOKEN=
AI_DAILY_PROMPT_LIMIT=0
AI_EMBEDDINGS_ENABLED=false
AI_MCP_ENABLED=false
```

`AI_DAILY_PROMPT_LIMIT=0` means no daily cap (see `config/ai.php`). Remote JSON import hosts must be allowlisted via `AI_REMOTE_IMPORT_HOSTS` when you use those tools.

Open the AI section in the admin UI once the app and gateway are up. Deeper behavior is covered in [AI assistant model](/docs/ai-assistant) and [AI configuration](/docs/ai-configuration).

## Useful Artisan follow-ups

```bash
# Re-sync permission definitions from the enum (ops / upgrades)
php artisan permissions:sync

# Optional: drop DB permissions no longer present on PermissionEnum
php artisan permissions:sync --prune

# Cleanup helpers (zips, uploads, AI)
php artisan files:cleanup-zips
php artisan files:cleanup-uploads
php artisan list | grep -E 'files:|ai:|permissions:'
```

Other operational commands (AI sync sources, AI attachment cleanup, chat verification) live under `app/Console/Commands` — run `php artisan list` for the full set.

## Verify the install

Smoke checklist:

1. Login with the seeded super admin.
2. Create a collection and a field; create an item.
3. Upload a small file in the file manager; confirm it appears after `storage:link`.
4. Trigger a zip of multiple files and confirm the queue worker processes `PrepareFilesZipJob`.
5. (Optional) Send an AI prompt that lists collections — confirms gateway + tools.

Automated tests:

```bash
php artisan test
# or
composer test
```

## Troubleshooting

### Create-project fails with Connection refused / migrate errors

Modern `post-create-project-cmd` only runs `key:generate` and prints the `externa:install` handoff — it must **not** migrate. If an older package still migrates during create-project and the DB is down, create-project aborts before the wizard.

**Recovery:** keep the project directory if Composer left it; `cd` in and run `php artisan externa:install`. Or recreate from a release that includes the minimal post-create scripts ([Packagist](/docs/packagist#troubleshooting-create-project)).

### `APP_KEY` / cipher errors

`.env` is missing or empty `APP_KEY`. From **1.2.2**, `php artisan externa:install` keeps a valid existing key or generates one when rewriting `.env` ([#249](https://github.com/qiick-io/externa-core/pull/249)). Otherwise run:

```bash
php artisan key:generate
```

### Migrate fails on SQLite

Ensure `database/database.sqlite` exists and `DB_CONNECTION=sqlite`. Remove stale `DB_DATABASE` host settings left over from MySQL examples if they confuse your tooling.

### Frontend is blank or outdated

Run Vite (`composer run dev` or `npm run dev`), or rebuild:

```bash
npm run build
```

If the site is on **Herd HTTPS** (`herd secure`) and the admin is black/blank, check that Vite `detectTls` uses the **site host** (so `public/hot` is `https://your-site.test:5173`, not `127.0.0.1`) — [Passkeys — Local HTTPS](/docs/passkeys#local-https-herd-valet-for-passkey-testing).

### 404 on uploaded file URLs

Run `php artisan storage:link` and confirm `FILESYSTEM_DISK` / public disk configuration.

### Jobs never run

Confirm `QUEUE_CONNECTION=database`, that migrations created queue tables, and that `queue:listen` or `queue:work` is running against the same database as the web process.

### AI tools “succeed” but nothing changes

Usually a weak local model inventing tool results. Switch `LOCAL_AI_MODEL` to a tool-capable model and watch `pail` / Laravel logs for real tool exceptions.

### Permission cache after seeding or role edits

Spatie caches permissions. Seeders call `forgetCachedPermissions`; if you edited the DB by hand:

```bash
php artisan permission:cache-reset
```

(or the equivalent provided by your installed spatie/laravel-permission version).

### Port already in use

Change the serve port (`php artisan serve --port=8001`) or stop the other process. Update `APP_URL` to match.

## Next steps

- Prefer the short path next time: [Quick Start](/docs/quick-start)
- Map the codebase: [Project layout](/docs/project-layout)
- Tune `.env`: [Environment variables](/docs/environment-variables)
- Understand access control: [Effective permissions](/docs/permissions)
- Hit the Public API / GraphQL: [externa-bruno](https://github.com/qiick-io/externa-bruno) (open in Bruno, env **Local**) · [Public CMS API](/docs/public-cms-api)
- Run background work in production: [Queues & scheduler](/docs/operations)
