Getting started

Installation

Interactive installer

Human happy path: composer create-projectphp 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:generateno 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

RequirementNotes
PHP 8.4+Matches composer.json (^8.4) and the repo .php-version pin.
Composer 2Used for composer setup and composer run dev.
Node.js + npmFrontend 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_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)

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
  1. Install Laravel Herd and ensure the CLI PHP version is 8.4+.
  2. Place the project (create-project or clone) in a directory Herd serves (or use herd link / park).
  3. 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
PromptEffect
APP_NAME / APP_URLWritten to .env
Databasesqlite (creates database/database.sqlite if missing), pgsql, mysql, or mariadb
Queue / broadcast profileMinimal (database queue, log broadcast — no Redis) or Full (redis queue, reverb — needs Redis)
SeedOptional db:seed (roles + super admin)
LOCAL_AI_URLOptional; blank skips
Thenkey: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:

  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.

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

Required for publicly served uploads and related assets:

php artisan storage:link

Seed roles, permissions, and super admin

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:

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:

ProcessCommand
serverphp artisan serve
queuephp artisan queue:listen --tries=1 --timeout=0
logsphp artisan pail --timeout=0
vitenpm 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):

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

  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

PathWhenHow
Herd (macOS)Native PHP/nginx + optional Pro Redis/ReverbRecommended: Laravel Herd + composer setup
Bare metalAny OS with PHP 8.4 + Composer + NodeSame as Herd steps without Herd hosts
Docker ComposeFull 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

ServiceRoleHost ports (defaults)
appnginx + php-fpm (development target)8000→80 (APP_PORT)
viteVite HMR (PHP+Node image)5173 (VITE_PORT)
pgsqlPostgreSQL 16 (default DB)5432
redisQueue / cache / session / Pulse / Reverb6379
horizonartisan horizon
reverbWebSockets8081→8080 (REVERB_HOST_PORT)
schedulerschedule:work
pulsepulse:work
mailpitLocal mail UI + SMTPUI 8025 / SMTP 1025
minioOptional S3 (--profile minio)API 9000 / console 9001
mysqlOptional (--profile mysql + overlay)3306
mariadbOptional (--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::

KnobDefaultPurpose
COMPOSE_APP_URLhttp://localhost:8000Forces container APP_URL so session/CSRF match the published URL even if host .env still has a Herd *.test URL
COMPOSE_DB_CONNECTIONpgsqlDB driver inside containers
COMPOSE_DB_HOSTpgsqlService DNS name (never 127.0.0.1 from a Herd .env)
COMPOSE_DB_PORT5432Port inside the network
COMPOSE_DB_DATABASE / COMPOSE_DB_USERNAME / COMPOSE_DB_PASSWORDexterna / externa / secretShared 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

  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:
docker compose exec app php artisan db:seed --force
  1. Open http://localhost:8000 and sign in with the seeded super admin (config/super_admin.php):
Default
Emailsuperadmin@example.com
Passwordpassword

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
EndpointMeaning
GET /health/liveProcess up
GET /health/readyDB (+ Redis when required) — 503 if not ready
GET /upLaravel framework liveness

Details: Deployment — Health probes · Operations.

Troubleshooting

SymptomLikely causeFix
Browser connection refused on :8000 during first minutesOld images without early nginx bind; or port not publishedPull 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 hostHost Herd APP_URL leaking into containersSet COMPOSE_APP_URL=http://localhost:8000 (Compose already overrides APP_URL)
Echo / Reverb fails from browserPort mismatch (Herd :8080 vs Compose :8081)Use VITE_REVERB_PORT=8081; restart vite after env changes
Blank UI / missing assetsvite stopped or never healthydocker compose logs vite; ensure app became healthy first
DB connection errors inside appDB_HOST=127.0.0.1 from Herd .envRely on COMPOSE_DB_* / service names (pgsql, mysql, mariadb)
MySQL/MariaDB profile still starts PostgresOverlay not appliedPass 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

  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:

RoleAccess summary
super-adminEvery permission
adminEvery permission
readerRead/show permissions only (can-show-*)
publicSystem / 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).

  1. Start LM Studio, vLLM, or another OpenAI-compatible server.
  2. Align .env with 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
  1. Prefer a model with reliable tool calling. The .env.example comment warns that tiny models often hallucinate success.
  2. 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:

  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:

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

Previous
Quick Start