Configuration

Files configuration

File manager storage and async thresholds are configured in config/files.php and config/filesystems.php. Env keys are documented under Environment variables; this page explains how disks, URLs, and cleanup interact.

Thresholds (config/files.php)

Config keyEnvDefaultMeaning
diskFILES_DISKassetsFile manager upload disk (assets local or s3 for S3/MinIO/R2).
duplicate_sync_max_bytesFILES_DUPLICATE_SYNC_MAX_BYTES52428800 (50 MB)Single non-folder files at or below this size are duplicated inline. Folders, bulk selections, and larger files are queued.
zip_ttl_minutesFILES_ZIP_TTL_MINUTES60Prepared zip archives under storage/app/zips expire after this many minutes.
zip_max_bytesFILES_ZIP_MAX_BYTES104857600 (100 MB)Maximum zip payload size enforced by the application.
FILES_DISK=assets
FILES_DUPLICATE_SYNC_MAX_BYTES=52428800
FILES_ZIP_TTL_MINUTES=60
FILES_ZIP_MAX_BYTES=104857600

Queue dependency

Queued duplicates and zip builds require a running queue worker (queue:listen / queue:work). See Operations.

Filesystem disks (config/filesystems.php)

DiskDriverRoot / notes
locallocalstorage/app/private — default private storage (FILESYSTEM_DISK defaults to local). serve enabled for authenticated streaming patterns.
publiclocalstorage/app/public — URL {APP_URL}/storage. Visibility public.
assetslocalstorage/app/public/assetspublic file-manager bytes. URL uses FILE_PUBLIC_URL_BASE (see below).
private_assetslocalstorage/app/private/assetseffective-private file-manager bytes. Not in storage:link; no public URL.
s3s3Optional cloud disk; scaffolded with standard AWS_* env vars.

Default disk:

FILESYSTEM_DISK=local
'links' => [
    public_path('storage') => storage_path('app/public'),
],

Create the link once per environment:

php artisan storage:link

Without this link, browsers cannot resolve /storage/... URLs for the public / assets disks. The private_assets disk is intentionally not linked.

Private vs public local bytes

When FILES_DISK=assets (default), Externa maps effective privacy onto the local pair:

Effective accessDiskBrowser URL
publicassets{FILE_PUBLIC_URL_BASE}/storage/assets/{storage_path}
privateprivate_assetsnone — admin download / API /api/v1/files/{id}/content

Visibility changes (updateMetadata / move into a private folder) move bytes between the two disks. One-shot migration for legacy private rows still on assets:

php artisan files:migrate-private-disk

When FILES_DISK=s3, objects stay on S3; private files still omit a static public URL and use auth'd paths (bucket ACL is the operator's responsibility).

FILE_PUBLIC_URL_BASE

At the top of config/filesystems.php:

$filePublicBase = rtrim((string) env('FILE_PUBLIC_URL_BASE', env('APP_URL', 'http://localhost')), '/');

The assets disk URL is:

{FILE_PUBLIC_URL_BASE}/storage/assets
VariableFallbackWhen to set
FILE_PUBLIC_URL_BASEAPP_URLCDN or different public origin for asset URLs while the app still runs on another host.
# Optional — omit to use APP_URL
FILE_PUBLIC_URL_BASE=https://cdn.example.com
APP_URL=https://app.example.com

The public disk URL still uses APP_URL directly ({APP_URL}/storage). Keep both consistent unless you intentionally split origins.

Optional S3 / MinIO

Set the file manager disk with FILES_DISK (default assets). Framework default FILESYSTEM_DISK stays local for private/local attachments.

FILES_DISK=s3
FILESYSTEM_DISK=local

AWS_ACCESS_KEY_ID=externa
AWS_SECRET_ACCESS_KEY=externasecret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=externa
AWS_ENDPOINT=http://minio:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
AWS_URL=http://localhost:9000/externa

Local Compose MinIO: docker compose --profile minio up (Installation — Docker).

Upload, download, where-used, thumbnails/transforms, and zip source reads go through the Storage disk API (S3-safe). Chat/AI temporary attachments default to the local disk; absolutePath() materializes remote objects to a temp file when needed.

Remaining limits (precise)

Still local / ephemeralWhy
Prepared zip archives under storage/app/zipsBuilt on a shared volume between app and Horizon; not sticky to one host bind for user files, but zips themselves are local ephemeral artifacts (TTL + files:cleanup-zips)
Chunk upload scratch on the chosen diskChunks live on FILES_DISK (S3 OK); session metadata is in DB
storage:link for assetsOnly required when FILES_DISK=assets (local public assets)

Multi-container

With FILES_DISK=s3, app replicas do not need a sticky host bind for CMS user files. Still share a storage volume (or accept zip loss) so prepared downloads and local chat temps work across Horizon workers.

Cleanup commands

Scheduled in routes/console.php (see Operations):

CommandSchedulePurpose
files:cleanup-uploadshourlyRemoves expired partial/chunked upload sessions and their chunk storage (CleanupStaleFileUploadsCommandFileService::cleanupStaleUploads()).
files:cleanup-zipshourlyRemoves prepared zip downloads older than zip_ttl_minutes (CleanupExpiredFileZipsCommandFileService::cleanupExpiredZips()).
files:migrate-private-diskmanualMoves effective-private local files from assetsprivate_assets (upgrade / one-shot).

Run manually:

php artisan files:cleanup-uploads
php artisan files:cleanup-zips
php artisan files:migrate-private-disk

Upload policy (app code, not env)

Thresholds above do not configure which file types are accepted. The File Manager uses a code-level extension denylist (ForbiddenUploadExtension, including svg / html / htm) plus PlainTextSanitizer on name/metadata/tags — see File manager — Upload security. Team chat uploads remain any-type until saved into Files.

JSON and Inertia file-manager endpoints live under /files/* with the can.manage.files middleware. Full route table: Admin API. Data model notes: File tree model.

Previous
Application config