Getting started
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.
Faster path
Operators: Packagist create-project then php artisan externa:install. Copy-paste Herd .env without the wizard: 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
| Requirement | Notes |
|---|---|
| PHP 8.4+ | Matches composer.json (^8.4) and the repo .php-version pin. |
| 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 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. Sail remains require-dev only. |
Optional for AI features:
- An OpenAI-compatible local gateway (e.g. LM Studio) listening where
LOCAL_AI_URLpoints (defaulthttp://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)
composer create-project qiick/externa-core:^1.0.0@beta my-externa
cd my-externa
nvm use
php artisan externa:install
Details: Packagist & create-project. Skip to Start the development stack when the installer already migrated, seeded, and built.
Contributors (git clone)
git clone https://github.com/qiick-io/externa-core.git
cd externa-core
nvm use
Recommended: Laravel Herd
- Install Laravel Herd and ensure the CLI PHP version is 8.4+.
- Place the project (create-project or clone) in a directory Herd serves (or use
herd link/ park). - Open a terminal in the project root for the steps below.
Herd is optional — any local PHP 8.4+ + 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).
Step-by-step install
Interactive installer (externa:install)
Preferred after create-project (or on a fresh clone):
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, log broadcast — no Redis) or Full (redis queue, reverb — needs Redis) |
| Seed | Optional db:seed (roles + super admin) |
| LOCAL_AI_URL | Optional; blank skips |
| Then | key:generate, migrate, storage:link, npm install / npm run build (unless --no-npm) |
Flags: --force (allow production-looking env / overwrite), --no-npm, --no-seed. Re-run asks before overwriting an existing .env (decline = leave env untouched). Refuses production-looking .env without --force.
Manual / CI: Composer setup
The setup script in composer.json is the non-interactive bootstrap (CI, scripted environments):
composer setup
That runs, in order:
composer install- Copy
.env.example→.envif.envis missing php artisan key:generatephp artisan migrate --forcenpm installnpm 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.
# 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:
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; confirm-password details: Confirm password with a passkey. Database matrix: Supported databases. File and AI tunables: Environment variables.
Link public storage
Required for publicly served uploads and related assets:
php artisan storage:link
Seed roles, permissions, and super admin
php artisan db:seed
DatabaseSeeder calls:
PermissionSeeder— permissions fromApp\Enums\PermissionEnumviapermissions:syncRoleSeeder— rolessuper-admin,admin,reader, and system rolepublicCreateSuperAdminSeeder— first login user fromconfig/super_admin.php
Override credentials before seeding if needed:
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.
Warning
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
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):
- Copy
BROADCAST_*/REVERB_*/VITE_REVERB_*/ Pulse keys from.env.example(Herd defaults on:8080). - Restart Vite after any
VITE_REVERB_*change. - Run the two long-lived workers (Herd Reverb is already up — do not also
reverb:start):
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).
Dev Container (VS Code / Codespaces)
Third official local path alongside Herd and Docker Compose:
- Open externa-core in VS Code or Codespaces.
- Reopen in Container (uses
.devcontainer/devcontainer.json+ rootcompose.yaml, serviceapp). - Wait for post-create (
composer install,npm ci, key, migrate, seed). - 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 + 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
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):
APP_PORT=8000
REVERB_HOST_PORT=8081
VITE_PORT=5173
RUN_MIGRATIONS=true
Profiles and DB overlays
MinIO (S3):
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. minio-init creates the bucket.
MySQL 8 (stops default Postgres via overlay — puts pgsql behind a non-default profile):
docker compose --profile mysql -f compose.yaml -f compose.mysql.yaml up --build
MariaDB (host :3307 like Herd):
docker compose --profile mariadb -f compose.yaml -f compose.mariadb.yaml up --build
If an old Postgres container still owns host :5432:
docker compose stop pgsql
Database matrix: 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
RUN_MIGRATIONS=true(default) runsphp artisan migrate --forcein theappentrypoint after DB is reachable.- Seed is not automatic. After the stack is healthy:
docker compose exec app php artisan db:seed --force
- Open
http://localhost:8000and sign in with the seeded super admin (config/super_admin.php):
| Default | |
|---|---|
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:
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 · 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:
docker compose ps
docker compose logs -f app
curl -fsS http://localhost:8000/health/ready
Production image + Compose: Deploy with Docker · Deployment — Docker. 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:
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
- Go to the login page (Fortify).
- Sign in with the seeded super admin (default
superadmin@example.com/password). - Confirm you can open Dashboard, Users, Collections, and Files according to
super-adminpermissions.
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 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:
php artisan queue:listen --tries=1 --timeout=0
Warning
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).
- Start LM Studio, vLLM, or another OpenAI-compatible server.
- Align
.envwith the gateway:
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
- Prefer a model with reliable tool calling. The
.env.examplecomment warns that tiny models often hallucinate success. - Optional hardening / features:
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 and AI configuration.
Useful Artisan follow-ups
# 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:
- Login with the seeded super admin.
- Create a collection and a field; create an item.
- Upload a small file in the file manager; confirm it appears after
storage:link. - Trigger a zip of multiple files and confirm the queue worker processes
PrepareFilesZipJob. - (Optional) Send an AI prompt that lists collections — confirms gateway + tools.
Automated tests:
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).
APP_KEY / cipher errors
.env is missing or empty APP_KEY. Run:
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:
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.
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:
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
- Map the codebase: Project layout
- Tune
.env: Environment variables - Understand access control: Effective permissions
- Hit the Public API / GraphQL: externa-bruno (open in Bruno, env Local) · Public CMS API
- Run background work in production: Queues & scheduler