Getting started
MarkdownEnvironment variables
Externa reads configuration from .env (copied from .env.example during composer setup). Most keys map into config/*.php via env(). This page groups the variables you will actually set for externa-core and documents defaults.
Note
After changing env values that are cached in production, run php artisan config:clear or rebuild config cache. Locally, composer test already clears config before Pest runs.
App
Standard Laravel application identity and runtime flags from .env.example:
| Variable | Default (example) | Purpose |
|---|---|---|
APP_NAME | Laravel | Application name. Used as mail from-name interpolation and as the fallback display name when Project settings → name is empty. Transactional mail branding (logo/color/header) comes from Appearance / Project settings — see Project settings. |
APP_ENV | local | Environment name (local, production, …). |
APP_KEY | (empty until generated) | Encryption key; composer setup runs key:generate. |
APP_DEBUG | false (1.4.0 .env.example; was true) | Detailed errors when true — keep false in production; turn on only while actively debugging. |
APP_URL | http://localhost | Canonical URL (scheme + host + port). Must match how you browse. App (password, TOTP, CMS) works on HTTP; passkeys need a secure context (HTTPS, or http://localhost / *.localhost — not http://*.test). Drives Fortify RP ID / allowed_origins. See Passkeys — Requirements. |
APP_LOCALE | en | Default locale. |
APP_FALLBACK_LOCALE | en | Fallback locale. |
APP_FAKER_LOCALE | en_US | Faker locale for factories/seed demos. |
APP_MAINTENANCE_DRIVER | file | Maintenance mode driver. |
APP_MAINTENANCE_STORE | (commented) | Optional store when using database maintenance driver. |
BCRYPT_ROUNDS | 12 | Password hashing rounds. |
LOG_CHANNEL | stack | Default log channel. |
LOG_STACK | single | Channels in the stack. |
LOG_DEPRECATIONS_CHANNEL | null | Deprecation log target. |
LOG_LEVEL | debug | Minimum log level. |
VITE_APP_NAME | "${APP_NAME}" | Exposed to the Vite/React build as the app name. |
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
VITE_APP_NAME="${APP_NAME}"
Database, session, cache, and queue
Externa’s .env.example defaults to SQLite, database sessions, and Redis for queue + cache (full stack). For light local without Redis, set QUEUE_CONNECTION=database and CACHE_STORE=database. Supported engines and versions: Supported databases.
| Variable | Default | Purpose |
|---|---|---|
DB_CONNECTION | sqlite | Driver: sqlite | pgsql | mysql | mariadb. See Supported databases. |
DB_HOST | 127.0.0.1 (commented) | Host for MySQL/Postgres. |
DB_PORT | 3306 (commented) | Port for non-SQLite drivers. |
DB_DATABASE | laravel (commented) | Database name / path depending on driver. |
DB_USERNAME | root (commented) | DB user. |
DB_PASSWORD | (empty, commented) | DB password. |
SESSION_DRIVER | database | Session storage. |
SESSION_LIFETIME | 120 | Minutes. |
SESSION_ENCRYPT | false | Encrypt session payload. |
SESSION_PATH | / | Cookie path. |
SESSION_DOMAIN | null | Cookie domain. |
QUEUE_CONNECTION | redis (fallback database) | Full stack uses Redis + Horizon. Minimal: database + queue:listen. |
CACHE_STORE | redis | Prefer Redis for app / public API cache; database is fine for light local. |
CACHE_PREFIX | (commented) | Optional key prefix. |
BROADCAST_CONNECTION | reverb (fallback log) | Full: Reverb. Minimal: log (60s notification poll only when realtime is off). |
FILESYSTEM_DISK | local | Framework default disk (private/local). |
FILES_DISK | assets | File manager disk (assets or s3). See Files configuration. |
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
# Full stack defaults in .env.example — switch to database/log for minimal:
BROADCAST_CONNECTION=reverb
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis
CACHE_STORE=redis
# CACHE_STORE=database # ok for light local without Redis cache
Redis (Herd / full stack)
Required for Horizon, recommended for app cache / public API performance, plus Reverb-friendly ops and Pulse redis ingest — see Redis prerequisite.
| Variable | Default |
|---|---|
REDIS_CLIENT | phpredis |
REDIS_HOST | 127.0.0.1 |
REDIS_PASSWORD | null |
REDIS_PORT | 6379 |
REDIS_CACHE_DB | 1 (app cache; keep off queue DB) |
REDIS_PULSE_DB | 2 (optional; connection pulse) |
MEMCACHED_HOST | 127.0.0.1 |
Reverb (Herd Pro defaults)
Match Herd → Services → Reverb (:8080). Host for Echo must be 127.0.0.1 or localhost (not 0.0.0.0). Restart Vite after changing VITE_REVERB_*.
| Variable | Default | Notes |
|---|---|---|
REVERB_APP_ID | 1001 | Herd shared app id |
REVERB_APP_KEY | laravel-herd | Shared with Vite |
REVERB_APP_SECRET | secret | Server secret |
REVERB_HOST | 127.0.0.1 | WS host for clients |
REVERB_PORT | 8080 | Herd Reverb port |
REVERB_SCHEME | http | http / https |
VITE_REVERB_APP_KEY | "${REVERB_APP_KEY}" | Echo key |
VITE_REVERB_HOST | "${REVERB_HOST}" | Echo host |
VITE_REVERB_PORT | "${REVERB_PORT}" | Echo port |
VITE_REVERB_SCHEME | "${REVERB_SCHEME}" | Echo TLS flag |
Standalone (no Herd Reverb): set your own REVERB_APP_* and run php artisan reverb:start.
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=1001
REVERB_APP_KEY=laravel-herd
REVERB_APP_SECRET=secret
REVERB_HOST=127.0.0.1
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
Pulse
| Variable | Default | Notes |
|---|---|---|
PULSE_ENABLED | true | Master switch |
PULSE_INGEST_DRIVER | redis | Or storage without Redis worker |
PULSE_REDIS_CONNECTION | pulse | Dedicated Redis connection |
PULSE_STORAGE_KEEP | 2 days | Trim window |
PULSE_INGEST_KEEP | 2 days | Ingest trim |
Warning
With Herd Reverb already on :8080, full local ops need horizon + pulse:work (do not also reverb:start). Standalone: composer run dev:full starts all three. Minimal: composer run dev + queue:listen. See Minimal vs full stack.
Local default is the log mailer (messages go to the log, not SMTP).
| Variable | Default | Purpose |
|---|---|---|
MAIL_MAILER | log | Mail transport. |
MAIL_SCHEME | null | Scheme when using SMTP-like transports. |
MAIL_HOST | 127.0.0.1 | SMTP host. |
MAIL_PORT | 2525 | SMTP port. |
MAIL_USERNAME | null | SMTP user. |
MAIL_PASSWORD | null | SMTP password. |
MAIL_FROM_ADDRESS | hello@example.com | From address. |
MAIL_FROM_NAME | "${APP_NAME}" | From display name. |
Logo/color/header branding for password reset and similar mail is not controlled by MAIL_* — see Project settings → Transactional email branding. Ensure logo files are publicly reachable (FILE_PUBLIC_URL_BASE / assets disk / CDN). Production provider .env examples: Mail providers.
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS / S3 / MinIO (optional)
For file-manager object storage set FILES_DISK=s3 (keep FILESYSTEM_DISK=local unless you intentionally move everything):
| Variable | Default | Notes |
|---|---|---|
AWS_ACCESS_KEY_ID | (empty) | |
AWS_SECRET_ACCESS_KEY | (empty) | |
AWS_DEFAULT_REGION | us-east-1 | |
AWS_BUCKET | (empty) | |
AWS_URL | (empty) | Public/base URL for the bucket |
AWS_ENDPOINT | (empty) | MinIO/R2 endpoint |
AWS_USE_PATH_STYLE_ENDPOINT | false | true for MinIO |
FILES_DISK=s3
AWS_ENDPOINT=http://minio:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
Compose MinIO profile: Installation — Docker.
Google Cloud Storage (optional, 1.4.0)
For FILES_DISK=gcs (config/filesystems.php → disks.gcs). Details: Files & storage — Google Cloud Storage.
| Variable | Default | Notes |
|---|---|---|
GOOGLE_CLOUD_PROJECT_ID | (empty) | GCP project id |
GOOGLE_CLOUD_KEY_FILE | (empty) | Service-account JSON path; empty = Application Default Credentials / GOOGLE_APPLICATION_CREDENTIALS |
GOOGLE_CLOUD_STORAGE_BUCKET | (empty) | Bucket name |
GOOGLE_CLOUD_STORAGE_PATH_PREFIX | (empty) | Optional key prefix |
GOOGLE_CLOUD_STORAGE_API_URI | (empty) | Optional public URI base |
GOOGLE_CLOUD_STORAGE_API_ENDPOINT | (empty) | Optional API endpoint (emulators) |
GOOGLE_CLOUD_STORAGE_VISIBILITY | private | Default object visibility |
FILES_DISK=gcs
GOOGLE_CLOUD_PROJECT_ID=
GOOGLE_CLOUD_KEY_FILE=
GOOGLE_CLOUD_STORAGE_BUCKET=
# GOOGLE_CLOUD_STORAGE_PATH_PREFIX=
# GOOGLE_CLOUD_STORAGE_API_URI=
# GOOGLE_CLOUD_STORAGE_API_ENDPOINT=
# GOOGLE_CLOUD_STORAGE_VISIBILITY=private
Azure Blob Storage (optional, 1.4.0)
For FILES_DISK=azure (config/filesystems.php → disks.azure). Details: Files & storage — Azure Blob Storage.
| Variable | Default | Notes |
|---|---|---|
AZURE_STORAGE_CONNECTION_STRING | (empty) | Shared-key connection string |
AZURE_STORAGE_CONTAINER | externa | Container name |
AZURE_STORAGE_PREFIX | (empty) | Optional blob prefix |
AZURE_STORAGE_URL | (empty) | Optional public base URL; its origin is added to the admin CSP img-src / media-src |
FILES_DISK=azure
AZURE_STORAGE_CONNECTION_STRING=
AZURE_STORAGE_CONTAINER=externa
# AZURE_STORAGE_PREFIX=
# AZURE_STORAGE_URL=
Docker Compose knobs
Used by compose.yaml / samples (.env.docker.example, .env.docker.prod.example). Compose environment: entries override host Herd .env values inside containers so DB_HOST=127.0.0.1 cannot poison the stack.
| Variable | Default (local sample) | Purpose |
|---|---|---|
COMPOSE_APP_URL | http://localhost:8000 | Overrides container APP_URL (session/CSRF must match published URL) |
COMPOSE_DB_CONNECTION | pgsql | Driver for app services |
COMPOSE_DB_HOST | pgsql | Service DNS (mysql / mariadb with overlays) |
COMPOSE_DB_PORT | 5432 | Port on the Compose network |
COMPOSE_DB_DATABASE | externa | Shared with DB container |
COMPOSE_DB_USERNAME | externa | Shared with DB container |
COMPOSE_DB_PASSWORD | secret | Shared with DB container |
APP_PORT | 8000 local / 8080 prod sample | Host publish for nginx |
REVERB_HOST_PORT | 8081 | Host publish for Reverb (avoids Herd :8080) |
VITE_PORT | 5173 | Host publish for Vite (local only) |
RUN_MIGRATIONS | true | Entrypoint runs migrate --force when true (default off in entrypoint) |
RUN_SEED | false | Entrypoint runs db:seed --force when true (admin via INITIAL_SUPER_ADMIN_*) |
CACHE_CONFIG | false local / true prod | Entrypoint config/route/view/event cache |
TRUSTED_PROXIES | (unset) | Set behind TLS-terminating proxy (* or CIDRs) — prod |
EXTERNA_IMAGE | ghcr.io/qiick-io/externa-core:latest | Override image for compose.quick.yaml |
Full walkthrough: Deploy with Docker · Installation — Docker · Deployment — Docker.
Files
Mapped in config/files.php:
| Variable | Default | Purpose |
|---|---|---|
FILES_DISK | assets | File manager disk name: assets, s3, gcs (1.4.0), or azure (1.4.0). |
FILES_DUPLICATE_SYNC_MAX_BYTES | 52428800 (50 MB) | Single non-folder files at or below this size duplicate synchronously. Folders, bulk selections, and larger files use DuplicateFilesJob. |
FILES_ZIP_TTL_MINUTES | 60 | Prepared zip lifetime under storage/app/zips before cleanup (files:cleanup-zips / related command). |
FILES_ZIP_MAX_BYTES | 104857600 (100 MB) | Maximum zip payload size enforced by application config. |
FILE_PUBLIC_URL_BASE | (optional) | Override base URL for public/asset disks (config/filesystems.php). Also documented under Files & storage. |
FILES_DISK=assets
FILES_DUPLICATE_SYNC_MAX_BYTES=52428800
FILES_ZIP_TTL_MINUTES=60
FILES_ZIP_MAX_BYTES=104857600
# FILE_PUBLIC_URL_BASE=
Note
Public URLs for the public disk still require php artisan storage:link regardless of these variables.
Outbound webhooks / Slack (1.3.0+)
| Variable | Default | Notes |
|---|---|---|
WEBHOOKS_ALLOW_PRIVATE_HOSTS | false | When false, reject loopback / RFC1918 / link-local / reserved hosts for webhook + Slack notifier URLs (save + delivery). Set true only for trusted LAN SSRF opt-in — Outbound webhooks. |
WEBHOOKS_ALLOW_PRIVATE_HOSTS=false
AI
Mapped primarily in config/ai.php. Externa defaults to a local OpenAI-compatible provider (LM Studio, vLLM, etc.).
| Variable | Default | Purpose |
|---|---|---|
AI_DEFAULT_PROVIDER | local | Provider key under config('ai.providers'). |
LOCAL_AI_URL | http://127.0.0.1:1234/v1 | Base URL for the local OpenAI-compatible API. |
LOCAL_AI_API_KEY | (empty) | Optional bearer key if your gateway requires one. |
LOCAL_AI_MODEL | openai/gpt-oss-20b in .env.example (local-model if unset in config) | Default text model for the local provider. Prefer tool-calling-capable models. |
LOCAL_AI_TRANSLATE_MODEL | (empty — falls back to LOCAL_AI_MODEL) | Optional faster non-thinking model for AI field translation (POST /ai/translate-fields), so the blocking request does not hit an nginx 504. Example: phi-4-mini-instruct. |
AI_ASSISTANT_PROVIDER | local | Provider the in-app Assistant (AppAssistant) runs on, a key under config('ai.providers') (1.4.0). Separate from AI_DEFAULT_PROVIDER, which other AI features use. |
OPENAI_REASONING_EFFORT | medium | Reasoning effort the Assistant asks OpenAI (and Azure) for: low, medium or high (1.4.0). Any other value, or empty, sends no reasoning option. |
ANTHROPIC_REASONING_EFFORT | medium | Anthropic extended thinking: low / medium / high = 2048 / 8192 / 24576 budget tokens (1.4.0). Any other value, or empty, turns thinking off. |
GEMINI_REASONING_EFFORT | medium | Gemini thinking level: low, medium or high, with thought summaries (1.4.0). Any other value, or empty, sends no thinking option. |
LOCAL_AI_REASONING_EFFORT | (unset = none) | Reasoning effort sent to the local OpenAI-compatible model (1.4.0). Unset keeps the model's own behaviour; set low / medium / high only when the model supports it. |
OPENAI_CONTEXT_WINDOW, ANTHROPIC_CONTEXT_WINDOW, GEMINI_CONTEXT_WINDOW, LOCAL_AI_CONTEXT_WINDOW | 400000, 200000, 1048576, 131072 | Context size in tokens shown next to an answer's token usage in the Assistant (1.4.0). Display only: it does not limit the request. |
AI_REMOTE_IMPORT_HOSTS | (empty) | Comma-separated host allowlist for remote JSON import tools. Empty means no remote hosts allowed. |
AI_WEBHOOK_TOKEN | (empty) | HMAC-SHA256 key for collection import webhook (X-AI-Webhook-Signature over the raw body only). Required whenever the webhook is used — empty fails closed in every env (1.3.0+). |
AI_WEBHOOK_TOKEN_PREVIOUS | (empty) | Previous HMAC key accepted only while AI_WEBHOOK_TOKEN_PREVIOUS_EXPIRES_AT is in the future (set by ai:rotate-webhook-token). |
AI_WEBHOOK_TOKEN_PREVIOUS_EXPIRES_AT | (empty) | ISO 8601 expiry for the previous token grace window. |
AI_DAILY_PROMPT_LIMIT | 0 | Max prompts per day; 0 disables the limit. |
AI_EMBEDDINGS_ENABLED | false | When true, SearchSimilarCollectionItems uses stored vectors (cosine); queues embed jobs on item save; falls back to semantic-lite without vectors / on provider failure. |
AI_MCP_ENABLED | false | Turns on the MCP server at /mcp (1.4.0; 404 while false) — MCP server. |
AI_MCP_WRITE_ENABLED | false | Registers MCP write tools (1.4.0). Still needs the mcp:write key scope, the AI write gate, and an approval. |
AI_MCP_RATE_LIMIT_PER_MINUTE | 60 | mcp rate limiter per API key / user / IP (1.4.0); a key's own rate_limit_per_minute wins. |
AI_TOOLS_ENABLED | true | false disables every AI tool in chat and MCP (1.4.0). |
AI_WRITE_TOOLS | false | Kill switch for write/destructive AI tools (1.4.0). Off = chat keeps read actions only — AI tool approvals & audit. |
AI_TOOLS_WRITE_ENABLED | false | Older name for AI_WRITE_TOOLS, read only when AI_WRITE_TOOLS is unset (1.4.0). |
AI_TOOLS_WRITE_ALLOW | (empty) | CSV of mutating tool class names allowed to run after approval, e.g. ManageFiles,ImportCollectionCsv; merged with Settings → AI write tools (1.4.0). |
AI_TOOLS_APPROVAL_TTL | 15 | Minutes before an MCP pending approval expires (1.4.0). |
AI_TOOLS_AUDIT_RETENTION_DAYS | 90 | Retention for ai_tool audit rows and resolved approvals, pruned daily by ai:prune-tool-audit (1.4.0). |
AI_TOOLS_REDACT_KEYS | built-in list (authorization, password, token, api_key, secret, email, phone, …) | CSV of argument keys redacted in the audit log and approval inbox (1.4.0; not in .env.example). |
AI_TOOLS_ALLOW | (empty) | When set, only these tool names are available (1.4.0). |
AI_TOOLS_DENY | (empty) | Tool names that are never available (1.4.0). |
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
# LOCAL_AI_TRANSLATE_MODEL=phi-4-mini-instruct
# Assistant provider, reasoning and context window (1.4.0)
# AI_ASSISTANT_PROVIDER=local
# LOCAL_AI_REASONING_EFFORT=
# OPENAI_REASONING_EFFORT=medium
# ANTHROPIC_REASONING_EFFORT=medium
# GEMINI_REASONING_EFFORT=medium
# LOCAL_AI_CONTEXT_WINDOW=131072
AI_REMOTE_IMPORT_HOSTS=
AI_WEBHOOK_TOKEN=
# AI_WEBHOOK_TOKEN_PREVIOUS=
# AI_WEBHOOK_TOKEN_PREVIOUS_EXPIRES_AT=
AI_DAILY_PROMPT_LIMIT=0
AI_EMBEDDINGS_ENABLED=false
AI_MCP_ENABLED=false
AI_MCP_WRITE_ENABLED=false
AI_MCP_RATE_LIMIT_PER_MINUTE=60
AI_TOOLS_ENABLED=true
AI_WRITE_TOOLS=false
AI_TOOLS_WRITE_ENABLED=false
AI_TOOLS_WRITE_ALLOW=
AI_TOOLS_APPROVAL_TTL=15
AI_TOOLS_AUDIT_RETENTION_DAYS=90
AI_TOOLS_ALLOW=
AI_TOOLS_DENY=
Rotation runbook: AI API — Rotate the webhook token.
Meilisearch (optional site search)
See Meilisearch sync.
| Variable | Default | Notes |
|---|---|---|
MEILISEARCH_ENABLED | false | Master switch |
MEILISEARCH_HOST | (empty) | e.g. http://127.0.0.1:7700 or http://meilisearch:7700 in Compose |
MEILISEARCH_API_KEY | (empty) | Master/admin key for indexing |
MEILISEARCH_INDEX | externa | Index uid; created with primaryKey=id if missing |
MEILISEARCH_INDEXABLE_STATUSES | published | Comma-separated; empty = ignore status field |
MEILISEARCH_ENABLED=false
MEILISEARCH_HOST=
MEILISEARCH_API_KEY=
MEILISEARCH_INDEX=externa
MEILISEARCH_INDEXABLE_STATUSES=published
Git export (optional content-as-code)
See Git export sync. Prefer webhooks for static rebuilds.
| Variable | Default | Notes |
|---|---|---|
GIT_EXPORT_ENABLED | false | Master switch |
GIT_EXPORT_REMOTE_URL | (empty) | SSH remote, e.g. git@github.com:org/content.git |
GIT_EXPORT_BRANCH | main | Push target branch |
GIT_EXPORT_SSH_KEY_PATH | (empty) | Deploy private key on disk |
GIT_EXPORT_FORMAT | json | json or markdown |
GIT_EXPORT_DRY_RUN | false | Commit locally, skip push |
GIT_EXPORT_COLLECTION_SLUGS | * | Comma-separated slugs or * |
GIT_EXPORT_ENABLED=false
GIT_EXPORT_REMOTE_URL=
GIT_EXPORT_BRANCH=main
GIT_EXPORT_SSH_KEY_PATH=
GIT_EXPORT_FORMAT=json
GIT_EXPORT_DRY_RUN=false
GIT_EXPORT_COLLECTION_SLUGS=*
Other AI providers (optional)
config/ai.php also documents keys for cloud providers when you switch AI_DEFAULT_PROVIDER (or call a named provider explicitly). These are not all listed in .env.example, but are read when present:
| Variable | Used by |
|---|---|
ANTHROPIC_API_KEY, ANTHROPIC_URL | Anthropic |
AZURE_OPENAI_API_KEY, AZURE_OPENAI_URL, AZURE_OPENAI_API_VERSION, AZURE_OPENAI_DEPLOYMENT, embedding/image deployment vars | Azure OpenAI |
AWS_BEDROCK_REGION, AWS_BEARER_TOKEN_BEDROCK, AWS_SESSION_TOKEN, AWS_USE_DEFAULT_CREDENTIALS | Bedrock (plus standard AWS keys) |
COHERE_API_KEY | Cohere |
DEEPSEEK_API_KEY | DeepSeek |
ELEVENLABS_API_KEY | ElevenLabs |
GEMINI_API_KEY, GEMINI_URL | Gemini |
GROQ_API_KEY | Groq |
JINA_API_KEY | Jina |
MISTRAL_API_KEY | Mistral |
OLLAMA_API_KEY, OLLAMA_URL | Ollama (http://localhost:11434) |
OPENAI_API_KEY, OPENAI_URL | OpenAI |
OPENROUTER_API_KEY | OpenRouter |
VOYAGEAI_API_KEY | Voyage |
XAI_API_KEY | xAI |
The local provider uses the openai driver with a custom url — laravel/ai accepts that pattern for OpenAI-compatible gateways.
Warning
Never commit real AI_WEBHOOK_TOKEN, cloud API keys, or production LOCAL_AI_API_KEY values. Rotate the HMAC secret with ai:rotate-webhook-token if it leaks (runbook); remote import without a tight AI_REMOTE_IMPORT_HOSTS allowlist is unsafe.
Seeding (initial super admin)
Read by config/super_admin.php and used only when CreateSuperAdminSeeder runs (via php artisan db:seed).
| Variable | Default | Purpose |
|---|---|---|
INITIAL_SUPER_ADMIN_FIRST_NAME | Super | First name. |
INITIAL_SUPER_ADMIN_LAST_NAME | Admin | Last name. |
INITIAL_SUPER_ADMIN_EMAIL | superadmin@example.com | Login email. |
INITIAL_SUPER_ADMIN_PASSWORD | password | Login password (local only). |
INITIAL_SUPER_ADMIN_FIRST_NAME=Super
INITIAL_SUPER_ADMIN_LAST_NAME=Admin
INITIAL_SUPER_ADMIN_EMAIL=superadmin@example.com
INITIAL_SUPER_ADMIN_PASSWORD=password
These keys may be omitted from .env.example because config defaults already match local development; set them explicitly before seeding shared environments.
Built-in roles created by seeders (not env vars): super-admin, admin, reader, public.
Public CMS API
| Variable | Default | Purpose |
|---|---|---|
API_KEY_RATE_LIMIT | 60 | Default requests/minute per API key when the key has no explicit limit (config/api.php). |
API_COST_LIMIT | 0 | Optional cost budget per 60 s window (0 = off). Weights: API_COST_BASE / API_COST_INCLUDE / API_COST_MUTATION (defaults 1 / 2 / 5). |
API_MAX_BODY_BYTES | 0 | Optional max Content-Length (0 = off). Over → 413. |
API_COST_FILTER_RELATION | 2 | Cost added per dotted relational filter key (filter[author.name]) when API_COST_LIMIT is on (1.4.0). |
API_MAX_BATCH_SIZE | 100 | Max entries per POST|PATCH|DELETE /api/v1/collections/{slug}/items/batch request, min 1 (1.4.0). |
API_MAX_PER_PAGE | 100 | Ceiling for REST/GraphQL list per_page / perPage. |
PUBLIC_API_CACHE_TTL | 120 | Public REST/GraphQL response-cache safety-net TTL in seconds (config/api.php response_cache_ttl, min 1) — Performance tuning. |
GRAPHQL_MAX_DEPTH | empty | Lighthouse max query depth; empty/unset = disabled. |
GRAPHQL_MAX_COMPLEXITY | empty | Lighthouse max query complexity; empty/unset = disabled. |
LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTION | false | When true, disable GraphQL introspection. |
CORS_ALLOWED_ORIGINS | * | Comma-separated origins for api/* when project public_api_allowed_origins is empty (config/cors.php). Prefer the project allowlist in production; see Origin allowlist. |
LIGHTHOUSE_QUERY_CACHE_ENABLE | true | Cache parsed GraphQL query strings (config/lighthouse.php). |
LIGHTHOUSE_QUERY_CACHE_MODE | opcache | Prefer opcache (default) over store when CACHE_STORE=database — avoids serializing AST into the DB cache. |
API_KEY_RATE_LIMIT=60
PUBLIC_API_CACHE_TTL=120
CORS_ALLOWED_ORIGINS=*
# API_COST_LIMIT=0
# API_MAX_BODY_BYTES=0
# API_MAX_PER_PAGE=100
# API_COST_FILTER_RELATION=2
# API_MAX_BATCH_SIZE=100
# GRAPHQL_MAX_DEPTH=
# GRAPHQL_MAX_COMPLEXITY=
Setup and auth model: Public CMS API. Abuse controls & scopes: API abuse controls. Cache TTL and include prewarm: Performance tuning.
Security headers (1.4.0)
From config/security.php — applied to admin HTML responses only (not /api/* or GraphQL). Details: Security headers.
| Variable | Default | Purpose |
|---|---|---|
SECURITY_HEADERS_ENABLED | true | Master switch for the headers below plus X-Content-Type-Options: nosniff |
SECURITY_X_FRAME_OPTIONS | DENY | X-Frame-Options |
SECURITY_REFERRER_POLICY | strict-origin-when-cross-origin | Referrer-Policy |
SECURITY_PERMISSIONS_POLICY | camera=(), microphone=(self), geolocation=(), payment=(), usb=() | Permissions-Policy; microphone=(self) keeps AI voice input working |
SECURITY_CSP | (empty) | Full Content-Security-Policy override; empty = built-in Vite/Reverb-friendly policy |
SECURITY_HEADERS_ENABLED=true
# SECURITY_X_FRAME_OPTIONS=DENY
# SECURITY_REFERRER_POLICY=strict-origin-when-cross-origin
# SECURITY_PERMISSIONS_POLICY="camera=(), microphone=(self), geolocation=(), payment=(), usb=()"
# SECURITY_CSP=
None of these keys are in .env.example; the defaults apply when they are unset.
Activity log
From config/activitylog.php (Spatie Laravel Activitylog):
| Variable | Default | Purpose |
|---|---|---|
ACTIVITYLOG_ENABLED | true | When false, activities are not persisted. |
ACTIVITYLOG_BUFFER_ENABLED | false | Buffer activities in memory and flush after the response — useful only for high-volume logging per request. |
ACTIVITYLOG_ENABLED=true
ACTIVITYLOG_BUFFER_ENABLED=false
Other activity log options (retention days, model class, soft-deleted subjects) are config-file defaults rather than .env keys — see config/activitylog.php (clean_after_days defaults to 365).
Optional integrations (1.2.0+)
All of these are off by default. When disabled, or when the host/remote is empty, the queue jobs do nothing.
OIDC / SSO
OIDC is configured in Project settings (DB), not in .env. The only env keys PHP reads are the Docker QA helpers and SSRF opt-in below (config/services.php):
| Variable | Default | Notes |
|---|---|---|
OIDC_DISCOVERY_BASE | — | Container-internal URL used to fetch discovery when the browser issuer is localhost (e.g. Keycloak in Compose) |
OIDC_SERVER_ORIGIN | — | Rewrites the server-side token/userinfo endpoints to this origin |
OIDC_ALLOW_PRIVATE_HOSTS | false | 1.4.0: allow discovery / endpoint URLs on private or CGNAT hosts (lab / Docker IdP). Off = SSRF guard rejects them |
Redirect URI to register at the IdP: {APP_URL}/auth/oidc/callback. See SSO / OIDC.
Meilisearch
| Variable | Default | Notes |
|---|---|---|
MEILISEARCH_ENABLED | false | Turns on the sync jobs |
MEILISEARCH_HOST | — | e.g. http://meilisearch:7700 (Compose --profile meilisearch) |
MEILISEARCH_API_KEY | — | Master or admin key (server-side only) |
MEILISEARCH_INDEX | externa | Index uid; created with primaryKey=id if missing |
MEILISEARCH_INDEXABLE_STATUSES | published | Comma list. Applies only when the item has a status field; empty = ignore |
See Meilisearch sync.
Git export
| Variable | Default | Notes |
|---|---|---|
GIT_EXPORT_ENABLED | false | Export on publish |
GIT_EXPORT_REMOTE_URL | — | SSH remote, e.g. git@github.com:org/content.git |
GIT_EXPORT_BRANCH | main | |
GIT_EXPORT_SSH_KEY_PATH | — | Deploy-key path on disk. Never commit the private key |
GIT_EXPORT_FORMAT | json | json or markdown |
GIT_EXPORT_DRY_RUN | false | Commit locally, skip push |
GIT_EXPORT_COLLECTION_SLUGS | * | Comma list of slugs, or * |
GIT_EXPORT_WORK_DIR | storage/app/git-export | Local clone |
GIT_EXPORT_COMMIT_NAME | Externa Git Export | Commit author name |
GIT_EXPORT_COMMIT_EMAIL | git-export@externa.local | Commit author email |
See Git export sync.
Quick reference: externa-specific block
Copy-paste block of the Externa-focused keys as they appear conceptually for a local AI-ready install:
# Files (assets | s3 | gcs | azure)
FILES_DISK=assets
FILES_DUPLICATE_SYNC_MAX_BYTES=52428800
FILES_ZIP_TTL_MINUTES=60
FILES_ZIP_MAX_BYTES=104857600
# Outbound webhooks / Slack SSRF (1.3.0+)
WEBHOOKS_ALLOW_PRIVATE_HOSTS=false
# FILE_PUBLIC_URL_BASE=
# AI (LM Studio / OpenAI-compatible local 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
# LOCAL_AI_TRANSLATE_MODEL=phi-4-mini-instruct
AI_REMOTE_IMPORT_HOSTS=
AI_WEBHOOK_TOKEN=
AI_DAILY_PROMPT_LIMIT=0
AI_EMBEDDINGS_ENABLED=false
# MCP server + AI tool write gate (1.4.0)
AI_MCP_ENABLED=false
AI_MCP_WRITE_ENABLED=false
AI_MCP_RATE_LIMIT_PER_MINUTE=60
AI_TOOLS_ENABLED=true
AI_WRITE_TOOLS=false
AI_TOOLS_WRITE_ALLOW=
AI_TOOLS_APPROVAL_TTL=15
AI_TOOLS_AUDIT_RETENTION_DAYS=90
AI_TOOLS_ALLOW=
AI_TOOLS_DENY=
# Security headers (1.4.0, defaults shown)
SECURITY_HEADERS_ENABLED=true
# Seeding (optional overrides — config defaults apply if omitted)
INITIAL_SUPER_ADMIN_EMAIL=superadmin@example.com
INITIAL_SUPER_ADMIN_PASSWORD=password
# Activity log
ACTIVITYLOG_ENABLED=true
ACTIVITYLOG_BUFFER_ENABLED=false
# Public CMS API
API_KEY_RATE_LIMIT=60
CORS_ALLOWED_ORIGINS=*
# API_COST_FILTER_RELATION=2
# API_MAX_BATCH_SIZE=100
Related docs
- Quick Start — Herd-oriented
.envand minimal commands - Installation — when
.envis created and seeded; Docker Compose local - Deployment — Docker production, health probes,
TRUSTED_PROXIES - Public CMS API —
/api/v1, keys, collection access - Files & storage — runtime file behavior
- AI configuration — providers and tool safety
- MCP server and AI tool approvals & audit — 1.4.0 AI keys
- Application config — non-env config surfaces