Getting started

Markdown

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

VariableDefault (example)Purpose
APP_NAMELaravelApplication 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_ENVlocalEnvironment name (local, production, …).
APP_KEY(empty until generated)Encryption key; composer setup runs key:generate.
APP_DEBUGfalse (1.4.0 .env.example; was true)Detailed errors when true — keep false in production; turn on only while actively debugging.
APP_URLhttp://localhostCanonical 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_LOCALEenDefault locale.
APP_FALLBACK_LOCALEenFallback locale.
APP_FAKER_LOCALEen_USFaker locale for factories/seed demos.
APP_MAINTENANCE_DRIVERfileMaintenance mode driver.
APP_MAINTENANCE_STORE(commented)Optional store when using database maintenance driver.
BCRYPT_ROUNDS12Password hashing rounds.
LOG_CHANNELstackDefault log channel.
LOG_STACKsingleChannels in the stack.
LOG_DEPRECATIONS_CHANNELnullDeprecation log target.
LOG_LEVELdebugMinimum 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.

VariableDefaultPurpose
DB_CONNECTIONsqliteDriver: sqlite | pgsql | mysql | mariadb. See Supported databases.
DB_HOST127.0.0.1 (commented)Host for MySQL/Postgres.
DB_PORT3306 (commented)Port for non-SQLite drivers.
DB_DATABASElaravel (commented)Database name / path depending on driver.
DB_USERNAMEroot (commented)DB user.
DB_PASSWORD(empty, commented)DB password.
SESSION_DRIVERdatabaseSession storage.
SESSION_LIFETIME120Minutes.
SESSION_ENCRYPTfalseEncrypt session payload.
SESSION_PATH/Cookie path.
SESSION_DOMAINnullCookie domain.
QUEUE_CONNECTIONredis (fallback database)Full stack uses Redis + Horizon. Minimal: database + queue:listen.
CACHE_STOREredisPrefer Redis for app / public API cache; database is fine for light local.
CACHE_PREFIX(commented)Optional key prefix.
BROADCAST_CONNECTIONreverb (fallback log)Full: Reverb. Minimal: log (60s notification poll only when realtime is off).
FILESYSTEM_DISKlocalFramework default disk (private/local).
FILES_DISKassetsFile 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.

VariableDefault
REDIS_CLIENTphpredis
REDIS_HOST127.0.0.1
REDIS_PASSWORDnull
REDIS_PORT6379
REDIS_CACHE_DB1 (app cache; keep off queue DB)
REDIS_PULSE_DB2 (optional; connection pulse)
MEMCACHED_HOST127.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_*.

VariableDefaultNotes
REVERB_APP_ID1001Herd shared app id
REVERB_APP_KEYlaravel-herdShared with Vite
REVERB_APP_SECRETsecretServer secret
REVERB_HOST127.0.0.1WS host for clients
REVERB_PORT8080Herd Reverb port
REVERB_SCHEMEhttphttp / 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

VariableDefaultNotes
PULSE_ENABLEDtrueMaster switch
PULSE_INGEST_DRIVERredisOr storage without Redis worker
PULSE_REDIS_CONNECTIONpulseDedicated Redis connection
PULSE_STORAGE_KEEP2 daysTrim window
PULSE_INGEST_KEEP2 daysIngest 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.

Mail

Local default is the log mailer (messages go to the log, not SMTP).

VariableDefaultPurpose
MAIL_MAILERlogMail transport.
MAIL_SCHEMEnullScheme when using SMTP-like transports.
MAIL_HOST127.0.0.1SMTP host.
MAIL_PORT2525SMTP port.
MAIL_USERNAMEnullSMTP user.
MAIL_PASSWORDnullSMTP password.
MAIL_FROM_ADDRESShello@example.comFrom 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):

VariableDefaultNotes
AWS_ACCESS_KEY_ID(empty)
AWS_SECRET_ACCESS_KEY(empty)
AWS_DEFAULT_REGIONus-east-1
AWS_BUCKET(empty)
AWS_URL(empty)Public/base URL for the bucket
AWS_ENDPOINT(empty)MinIO/R2 endpoint
AWS_USE_PATH_STYLE_ENDPOINTfalsetrue 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.

VariableDefaultNotes
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_VISIBILITYprivateDefault 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.

VariableDefaultNotes
AZURE_STORAGE_CONNECTION_STRING(empty)Shared-key connection string
AZURE_STORAGE_CONTAINERexternaContainer 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.

VariableDefault (local sample)Purpose
COMPOSE_APP_URLhttp://localhost:8000Overrides container APP_URL (session/CSRF must match published URL)
COMPOSE_DB_CONNECTIONpgsqlDriver for app services
COMPOSE_DB_HOSTpgsqlService DNS (mysql / mariadb with overlays)
COMPOSE_DB_PORT5432Port on the Compose network
COMPOSE_DB_DATABASEexternaShared with DB container
COMPOSE_DB_USERNAMEexternaShared with DB container
COMPOSE_DB_PASSWORDsecretShared with DB container
APP_PORT8000 local / 8080 prod sampleHost publish for nginx
REVERB_HOST_PORT8081Host publish for Reverb (avoids Herd :8080)
VITE_PORT5173Host publish for Vite (local only)
RUN_MIGRATIONStrueEntrypoint runs migrate --force when true (default off in entrypoint)
RUN_SEEDfalseEntrypoint runs db:seed --force when true (admin via INITIAL_SUPER_ADMIN_*)
CACHE_CONFIGfalse local / true prodEntrypoint config/route/view/event cache
TRUSTED_PROXIES(unset)Set behind TLS-terminating proxy (* or CIDRs) — prod
EXTERNA_IMAGEghcr.io/qiick-io/externa-core:latestOverride image for compose.quick.yaml

Full walkthrough: Deploy with Docker · Installation — Docker · Deployment — Docker.

Files

Mapped in config/files.php:

VariableDefaultPurpose
FILES_DISKassetsFile manager disk name: assets, s3, gcs (1.4.0), or azure (1.4.0).
FILES_DUPLICATE_SYNC_MAX_BYTES52428800 (50 MB)Single non-folder files at or below this size duplicate synchronously. Folders, bulk selections, and larger files use DuplicateFilesJob.
FILES_ZIP_TTL_MINUTES60Prepared zip lifetime under storage/app/zips before cleanup (files:cleanup-zips / related command).
FILES_ZIP_MAX_BYTES104857600 (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+)

VariableDefaultNotes
WEBHOOKS_ALLOW_PRIVATE_HOSTSfalseWhen 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.).

VariableDefaultPurpose
AI_DEFAULT_PROVIDERlocalProvider key under config('ai.providers').
LOCAL_AI_URLhttp://127.0.0.1:1234/v1Base URL for the local OpenAI-compatible API.
LOCAL_AI_API_KEY(empty)Optional bearer key if your gateway requires one.
LOCAL_AI_MODELopenai/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_PROVIDERlocalProvider 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_EFFORTmediumReasoning 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_EFFORTmediumAnthropic extended thinking: low / medium / high = 2048 / 8192 / 24576 budget tokens (1.4.0). Any other value, or empty, turns thinking off.
GEMINI_REASONING_EFFORTmediumGemini 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_WINDOW400000, 200000, 1048576, 131072Context 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_LIMIT0Max prompts per day; 0 disables the limit.
AI_EMBEDDINGS_ENABLEDfalseWhen true, SearchSimilarCollectionItems uses stored vectors (cosine); queues embed jobs on item save; falls back to semantic-lite without vectors / on provider failure.
AI_MCP_ENABLEDfalseTurns on the MCP server at /mcp (1.4.0; 404 while false) — MCP server.
AI_MCP_WRITE_ENABLEDfalseRegisters 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_MINUTE60mcp rate limiter per API key / user / IP (1.4.0); a key's own rate_limit_per_minute wins.
AI_TOOLS_ENABLEDtruefalse disables every AI tool in chat and MCP (1.4.0).
AI_WRITE_TOOLSfalseKill switch for write/destructive AI tools (1.4.0). Off = chat keeps read actions only — AI tool approvals & audit.
AI_TOOLS_WRITE_ENABLEDfalseOlder 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_TTL15Minutes before an MCP pending approval expires (1.4.0).
AI_TOOLS_AUDIT_RETENTION_DAYS90Retention for ai_tool audit rows and resolved approvals, pruned daily by ai:prune-tool-audit (1.4.0).
AI_TOOLS_REDACT_KEYSbuilt-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.

See Meilisearch sync.

VariableDefaultNotes
MEILISEARCH_ENABLEDfalseMaster 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_INDEXexternaIndex uid; created with primaryKey=id if missing
MEILISEARCH_INDEXABLE_STATUSESpublishedComma-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.

VariableDefaultNotes
GIT_EXPORT_ENABLEDfalseMaster switch
GIT_EXPORT_REMOTE_URL(empty)SSH remote, e.g. git@github.com:org/content.git
GIT_EXPORT_BRANCHmainPush target branch
GIT_EXPORT_SSH_KEY_PATH(empty)Deploy private key on disk
GIT_EXPORT_FORMATjsonjson or markdown
GIT_EXPORT_DRY_RUNfalseCommit 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:

VariableUsed by
ANTHROPIC_API_KEY, ANTHROPIC_URLAnthropic
AZURE_OPENAI_API_KEY, AZURE_OPENAI_URL, AZURE_OPENAI_API_VERSION, AZURE_OPENAI_DEPLOYMENT, embedding/image deployment varsAzure OpenAI
AWS_BEDROCK_REGION, AWS_BEARER_TOKEN_BEDROCK, AWS_SESSION_TOKEN, AWS_USE_DEFAULT_CREDENTIALSBedrock (plus standard AWS keys)
COHERE_API_KEYCohere
DEEPSEEK_API_KEYDeepSeek
ELEVENLABS_API_KEYElevenLabs
GEMINI_API_KEY, GEMINI_URLGemini
GROQ_API_KEYGroq
JINA_API_KEYJina
MISTRAL_API_KEYMistral
OLLAMA_API_KEY, OLLAMA_URLOllama (http://localhost:11434)
OPENAI_API_KEY, OPENAI_URLOpenAI
OPENROUTER_API_KEYOpenRouter
VOYAGEAI_API_KEYVoyage
XAI_API_KEYxAI

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

VariableDefaultPurpose
INITIAL_SUPER_ADMIN_FIRST_NAMESuperFirst name.
INITIAL_SUPER_ADMIN_LAST_NAMEAdminLast name.
INITIAL_SUPER_ADMIN_EMAILsuperadmin@example.comLogin email.
INITIAL_SUPER_ADMIN_PASSWORDpasswordLogin 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

VariableDefaultPurpose
API_KEY_RATE_LIMIT60Default requests/minute per API key when the key has no explicit limit (config/api.php).
API_COST_LIMIT0Optional 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_BYTES0Optional max Content-Length (0 = off). Over → 413.
API_COST_FILTER_RELATION2Cost added per dotted relational filter key (filter[author.name]) when API_COST_LIMIT is on (1.4.0).
API_MAX_BATCH_SIZE100Max entries per POST|PATCH|DELETE /api/v1/collections/{slug}/items/batch request, min 1 (1.4.0).
API_MAX_PER_PAGE100Ceiling for REST/GraphQL list per_page / perPage.
PUBLIC_API_CACHE_TTL120Public REST/GraphQL response-cache safety-net TTL in seconds (config/api.php response_cache_ttl, min 1) — Performance tuning.
GRAPHQL_MAX_DEPTHemptyLighthouse max query depth; empty/unset = disabled.
GRAPHQL_MAX_COMPLEXITYemptyLighthouse max query complexity; empty/unset = disabled.
LIGHTHOUSE_SECURITY_DISABLE_INTROSPECTIONfalseWhen 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_ENABLEtrueCache parsed GraphQL query strings (config/lighthouse.php).
LIGHTHOUSE_QUERY_CACHE_MODEopcachePrefer 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.

VariableDefaultPurpose
SECURITY_HEADERS_ENABLEDtrueMaster switch for the headers below plus X-Content-Type-Options: nosniff
SECURITY_X_FRAME_OPTIONSDENYX-Frame-Options
SECURITY_REFERRER_POLICYstrict-origin-when-cross-originReferrer-Policy
SECURITY_PERMISSIONS_POLICYcamera=(), 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):

VariableDefaultPurpose
ACTIVITYLOG_ENABLEDtrueWhen false, activities are not persisted.
ACTIVITYLOG_BUFFER_ENABLEDfalseBuffer 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):

VariableDefaultNotes
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_HOSTSfalse1.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

VariableDefaultNotes
MEILISEARCH_ENABLEDfalseTurns 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_INDEXexternaIndex uid; created with primaryKey=id if missing
MEILISEARCH_INDEXABLE_STATUSESpublishedComma list. Applies only when the item has a status field; empty = ignore

See Meilisearch sync.

Git export

VariableDefaultNotes
GIT_EXPORT_ENABLEDfalseExport on publish
GIT_EXPORT_REMOTE_URL—SSH remote, e.g. git@github.com:org/content.git
GIT_EXPORT_BRANCHmain
GIT_EXPORT_SSH_KEY_PATH—Deploy-key path on disk. Never commit the private key
GIT_EXPORT_FORMATjsonjson or markdown
GIT_EXPORT_DRY_RUNfalseCommit locally, skip push
GIT_EXPORT_COLLECTION_SLUGS*Comma list of slugs, or *
GIT_EXPORT_WORK_DIRstorage/app/git-exportLocal clone
GIT_EXPORT_COMMIT_NAMEExterna Git ExportCommit author name
GIT_EXPORT_COMMIT_EMAILgit-export@externa.localCommit 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