Configuration
Laracrate is configured entirely through one published file, config/laracrate.php. Publish it during installation (see the Installation section), then shape it to your app. The defaults are safe and runnable as-is: every key below has a working default, so you only override what your app actually needs.
This section walks the file top to bottom. For the deeper behavior that some keys drive (the pipeline, embeddings, watermarks), the relevant H2 section is cross-referenced rather than re-explained here.
php artisan vendor:publish --tag=laracrate-config
Default collection and context
Applied to the schema when a File row is inserted without an explicit collection or context. This happens, for example, when a variant is created and inherits from its parent. Any string is valid; the convention is default. Changing these values updates the column DEFAULT only if you re-run the migration.
'default_collection' => 'default', 'default_context' => 'default',
Defaults per file type
The defaults block declares the safe-by-default allowlist of MIME types, extensions, max sizes, and processing options for each of the four file types (image, document, audio, video). Any collection that does not declare its own accepted_mime_types or accepted_extensions inherits these.
Some formats are deliberately excluded from the allowlist and must be opted into explicitly per collection:
- SVG, because it can carry
<script>. - ICO, because of legacy CVEs.
- HTML, JS, PHP, EXE, BAT, SH, because they are executable.
- ZIP, RAR, 7Z, because they are containers with zip-slip and hidden-content vectors.
To allow any of these, override accepted_mime_types and accepted_extensions in the specific collection along with your own validation policy.
| Type | max_file_size (KB) |
Notable defaults |
|---|---|---|
image |
10240 (10 MB) |
format: webp, quality: 90, variant_quality: 85, max_width: 1920, max_height: 1080, plus thumbnail (300x300), medium (800x800), large (1600x1600) variants |
document |
20480 (20 MB) |
PDF, Word, OpenDocument, RTF, plain text, Markdown, Excel, CSV, PowerPoint, EPUB. No variants (documents use rasterized previews) |
audio |
5120 (5 MB) |
MP3, WAV, OGG, M4A, FLAC, AAC, Opus, WebM |
video |
102400 (100 MB) |
MP4, MOV, WebM, M4V, MKV, AVI, OGV |
The image accepted_extensions are jpeg, jpg, png, gif, webp, heic, heif, bmp, tiff. See the Images, variants and watermarks section for how format, quality, and variants drive processing.
Collections
A collection is the upload policy for one business context (avatars, a gallery, identity documents). Each entry declares where files land, who can read them, and how they are processed. Collections are the central concept of the package, so this is the largest block.
'collections' => [ 'avatar' => [ 'disk' => 'media', 'access' => 'public', 'single' => true, 'types' => [ 'image' => [ 'variants' => [ 'small' => ['width' => 64, 'height' => 64, 'fit' => true], 'medium' => ['width' => 128, 'height' => 128, 'fit' => true], 'large' => ['width' => 256, 'height' => 256, 'fit' => true], ], ], ], ], ],
The package ships four example collections (avatar, gallery, documents, identity) so you can see the shape. Replace them with your own.
Anatomy of a collection entry
| Key | Type | Purpose |
|---|---|---|
disk |
string | The Storage::disk() name from your config/filesystems.php. The package never duplicates credentials; it resolves this disk. There is no default, a missing disk is intentionally an error. |
access |
string | public or private (the default). Who the files are for; how a private one is served is chosen where it is used (url() or streamUrl()). The 1.x values signed and stream still read as private, stream also as sensitive. See the Displaying files and Access control sections. |
single |
bool | true keeps one file per owner. Replacing the file removes the previous one. |
sensitive |
bool | The files never get a direct link: url() returns the stream route, which checks the view policy and logs each access. See the Sensitive content section. |
encrypt |
bool | Encrypts the binary before it is stored on the backend. See the Sensitive content and encryption section. |
ttl_hours |
int | Files in the collection expire after this many hours. The laracrate:purge-expired command (run hourly) deletes them, cascading to variants and the backend binary. Useful for temp_uploads, expiring exports, or unpromoted drafts. |
quota_bytes |
int | Storage limit your app can check with UsageReporter before accepting more uploads. The package does not enforce quotas itself. See the Multi-tenancy, buckets and usage section. |
track_usage |
bool | Maintain live per-owner usage counters (file count and total bytes) for this collection in the laracrate_folderables table, updated by the file observer on create and delete, and rebuildable with laracrate:recompute-usage. See the Multi-tenancy, buckets and usage section. |
component |
string | Blade component used for default rendering. |
placeholder |
string | Fallback asset when the file does not exist (highest priority in placeholder resolution, below). |
types |
array | Per-type processing config (image, document, audio, video), keyed by type. Each type block can carry accepted_mime_types, accepted_extensions, max_file_size, variants, preview, and so on, overriding the global defaults. The image block's modes (['default', 'sphere']) says whether 360° photos are told apart. |
variants |
array | Inside a type block. Image variants: ['name' => ['width' => ..., 'height' => ..., 'fit' => bool, 'watermark' => bool]] (see the Images, variants and watermarks section). Video and PDF variants carry a type (video, image, frames); see the Video and PDF variants section. |
preview |
string | Inside a type block. The name of one of its variants (dot notation for nested ones, 'portada.mini'): $file->preview() returns it. See the Video and PDF variants section. |
extract / extract_text |
bool, string or array | When to extract text from the file: 'always' (true), 'on_demand' or 'never' (false), or a list of types. Can also be set per type inside types (extract_text is a legacy alias for extract). See the per-type extraction note below and the Text extraction section. |
embed |
bool, string or array | Whether to generate embeddings, with the same values as extract. See the Text extraction, embeddings and search section. |
actions |
array | Custom actions to attach to the collection. |
models |
array | Per-model scoping (covered next). |
The models block: per-model scoping
By default any model using HasFiles can write to a collection with the same config. Declaring models restricts the collection to specific owner types and merges a per-model override on top of the base config. Resolution lives in EduLazaro\Laracrate\Support\CollectionConfig::resolve().
'documents' => [ 'disk' => 'documents', 'access' => 'private', 'models' => [ // Cases never hand out a direct link. 'case' => ['sensitive' => true], // Organizations accept larger documents. 'organization' => ['types' => ['document' => ['max_file_size' => 51200]]], ], ],
Semantics when models is present:
- Only the listed keys may use the collection. Keys are matched against the morph alias or the fully qualified class name, normalized through
Relation::morphMap(). A model not listed triggersEduLazaro\Laracrate\Exceptions\CollectionNotAllowedForModel. - The per-model override is merged over the base with
array_replace_recursive, so nested structures (likevariants) merge key by key. - The
modelskey itself is stripped from the resolved config. - A per-model override cannot relocate the binary. The object key is always
/{id}/{collection}/{file}(tenant-prefixed when the file has a tenant), built byCreateFileAction. It is not configurable through apathkey. - Resolving with no model (
CollectionConfig::resolve($collection)with a null second argument) returns the base config without merging, which is what tooling that iterates collections without a model context receives.
You can check whether a collection is scoped with CollectionConfig::isRestricted($collection).
Per-type extract and embed
extract, embed and, for videos, visual resolve to one of three modes through EduLazaro\Laracrate\Support\ExtractionResolver:
| Value | Mode | Meaning |
|---|---|---|
'always' or true |
always | Runs when the file is uploaded. |
'on_demand' |
on demand | Runs only once someone requests it with $file->requestExtraction(). |
'never' or false |
never | Never runs. |
Declare them for the whole collection or per type inside types. The type wins:
'documents' => [ 'extract' => 'always', // default for every type 'embed' => true, 'types' => [ 'document' => ['max_file_size' => 51200], 'audio' => ['max_file_size' => 51200, 'extract' => 'on_demand'], 'video' => ['max_file_size' => 204800, 'extract' => 'on_demand', 'visual' => 'never'], ], ],
The collection-level value also accepts a list of types, which means 'always' for those types and 'never' for the rest. It matches the file's type by exact value or by the type. prefix:
'extract' => ['document', 'image'] // only these file types 'extract' => ['document', 'video.visual'] // videos too, with frame description
extract decides whether the text pipeline runs for a file and embed whether its text is embedded, so embed never extracts a type that extract excludes. When extract is not declared at all, the pipeline follows embed. visual describes video frames on top of the transcript and only applies when the video is extracted. The legacy boolean key extract_text is still honored. An unknown value throws an InvalidArgumentException.
Apps can register a per-file override with ExtractionResolver::setOverrideResolver(callable). The callable receives the File and returns an array in the same shape as the collection config (or null), for example per organization or per case. Whatever the override declares wins over the collection config, and its types blocks are merged type by type. See the Text extraction section.
On-demand extraction
On-demand types are skipped when the file is uploaded: the file ends up completed, with no text. Request the processing when it is needed, from a menu action or from an agent tool:
if ($file->canRequestExtraction()) { $file->requestExtraction(auth()->user()); // queued $file->requestExtraction(auth()->user(), sync: true); // inline, e.g. inside a tool }
requestExtraction() records who asked and when in the file's metadata (extraction_requested_at, extraction_requested_by_type, extraction_requested_by_id), which turns the on-demand modes on for that file, and processes it again from the semantic extraction band (priority 60). The original is not transcoded again and its variants are not regenerated. It returns false when there is nothing on demand for the file. canRequestExtraction() is true until the file has been requested, and again if that processing failed.
Placeholders
The fallback chain when a file or variant does not exist or is not the type the render expects. Override these in your published config.
'placeholders' => [ 'default' => '/img/laracrate/file.svg', 'image' => '/img/laracrate/image.svg', 'video' => '/img/laracrate/video.svg', 'audio' => '/img/laracrate/audio.svg', 'document' => '/img/laracrate/document.svg', ],
Resolution runs from most specific to most general:
config('laracrate.collections.{name}.placeholder')config('laracrate.placeholders.{type}')config('laracrate.placeholders.default')
Dynamic placeholders (initials avatars, generated SVGs)
A placeholder can be a callable instead of a string. When it is, fileLink() and fileRender() invoke it with (string $collection, string $type, Model $model) and use the returned string as the URL. This is how you render a generated fallback (an initials avatar, a ui-avatars URL, a per-model SVG) when a model has no file.
Use a callable array ([Class::class, 'method']), not a Closure. Closures are not serializable, so a Closure placeholder breaks php artisan config:cache in production.
// config/laracrate.php 'collections' => [ 'avatar' => [ 'disk' => 'media', 'access' => 'public', 'single' => true, 'placeholder' => [\App\Support\InitialsAvatar::class, 'placeholderFor'], ], ],
// app/Support/InitialsAvatar.php namespace App\Support; class InitialsAvatar { // The signature the package calls: (collection, type, model). public static function placeholderFor($collection, $type, $model): ?string { return self::dataUri($model?->name); } public static function dataUri(?string $name, int $size = 200): ?string { $name = trim((string) $name); if ($name === '') { return null; } $initials = strtoupper(mb_substr($name, 0, 1)); // 1 to 2 letters from the name $bg = '#1E40AF'; // derive a color from the name if you want variety $font = (int) round($size * 0.42); $svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' . $size . ' ' . $size . '">' . '<rect width="100%" height="100%" fill="' . $bg . '"/>' . '<text x="50%" y="50%" fill="#fff" font-size="' . $font . '" font-family="sans-serif"' . ' text-anchor="middle" dominant-baseline="central">' . htmlspecialchars($initials, ENT_QUOTES | ENT_XML1) . '</text></svg>'; return 'data:image/svg+xml;base64,' . base64_encode($svg); } }
Now $user->fileLink('avatar', 'small') returns the uploaded avatar when it exists, or the inline initials SVG when it does not, with no null checks in your views. A callable placeholder needs the model, so it resolves only through fileLink() and fileRender() (which carry it), not through the bare $file->placeholderFor(). String placeholders work everywhere.
URL strategy
How long the signed link of a private file lasts when the caller does not say. $file->url(expires: ...) sets it per call; streamUrl() and downloadUrl() are not signed and never expire. See the Displaying files section.
'urls' => [ 'signed_ttl' => 5, 'signed_cache_ttl' => 4, ],
| Key | Default | Meaning |
|---|---|---|
signed_ttl |
5 |
Minutes the default signed link of a private file stays valid. |
signed_cache_ttl |
4 |
Minutes that default link is cached server-side, so a page with many files signs each one once. A link with its own expires is not cached. |
Policies
The package uses PolicyRegistry as the canonical place to declare authorization per fileable_type. When register_gate is true (default), the service provider also binds FilePolicy to the Laravel Gate so you can use the native ergonomics.
'policies' => [ 'register_gate' => true, ],
With the bridge on, the Gate abilities view, update, and delete map to the registry methods canView, canEdit, and canDelete, so @can('view', $file), $user->can('update', $file), and Route::middleware('can:view,file') all work. Set register_gate => false if your app already registers its own FilePolicy. See the Access control and authorization section.
Streaming
Routing and audit options for the routes that serve a file through PHP (streamUrl(), downloadUrl()) and the viewer's data.
'stream' => [ 'route_prefix' => 'laracrate/files', 'route_name_prefix' => 'laracrate.files', 'middleware' => ['web', 'auth'], 'increment_downloads' => true, 'log_access' => true, ],
The laracrate/files prefix avoids collisions with an existing FileController under /files/..., which is common. Keep auth in the middleware: these routes carry no signature, so the session and the view policy are what let a request in. increment_downloads bumps the download counter on download, and log_access logs each access. See the HTTP endpoints section.
Status polling
Endpoints to poll processing status after an async upload.
'status' => [ 'route_prefix' => 'laracrate/files', 'middleware' => ['web', 'auth'], ],
GET /laracrate/files/{slug}/status returns one file, POST /laracrate/files/status accepts a batch of slugs. See the HTTP endpoints section.
Uploads
Routing and a disk allowlist for the direct presigned upload endpoints. The multipart block inherits this middleware when its own is null.
'uploads' => [ 'route_prefix' => 'laracrate/uploads', 'middleware' => ['web', 'auth'], 'allowed_disks' => [], 'targets' => base_path('routes/uploads.php'), ],
| Key | Default | Meaning |
|---|---|---|
route_prefix |
laracrate/uploads |
URL prefix for the upload endpoints (presign, cancel, and the grant based sign, confirm and receive). |
middleware |
['web', 'auth'] |
Middleware for the upload route group. Authorization is your app's responsibility. |
allowed_disks |
[] |
Allowlist of disks the plain presign and cancel endpoints accept. Empty means the disks your configured collections use, never any other disk (public, local...). The grant based endpoints do not read it: their disk always comes from the grant's collection. |
targets |
routes/uploads.php |
The file where the app declares its upload targets for the React and Svelte components, read the first time one is asked for. A published config without the key has none. |
A collection may also say how the components send its bytes with 'upload' => 'direct' (presigned PUT to the bucket) or 'upload' => 'server' (through PHP). Without it each component uses its own default: the dropzones go direct, the single file uploader goes through the server. A collection with 'encrypt' => true always goes through the server, the only place it can be encrypted.
These are the defaults; override them in your published config to restrict allowed_disks or change the prefix or middleware. See the HTTP endpoints and Upload modes sections.
Multipart upload
Tuning for large uploads to S3/R2. The server does not force multipart; the frontend chooses based on file.size. These values are the recommended thresholds.
'multipart' => [ 'threshold' => 100 * 1024 * 1024, // 100 MB 'part_size' => 10 * 1024 * 1024, // 10 MB 'expire_minutes' => 60, 'url_ttl_minutes' => 60, 'route_prefix' => 'laracrate/multipart', 'middleware' => null, ],
| Key | Default | Meaning |
|---|---|---|
threshold |
100 MB | Below this the client should use a single presigned PUT, at or above it multipart. |
part_size |
10 MB | Bytes per part (S3 minimum is 5 MB). 10 MB means 100 parts for 1 GB, 800 for 8 GB. |
expire_minutes |
60 |
TTL of the multipart session. After it, laracrate:abort-stale-multipart aborts it. |
url_ttl_minutes |
60 |
TTL of the per-part presigned URLs. |
route_prefix |
laracrate/multipart |
URL prefix for the multipart endpoints. |
middleware |
null |
Middleware for the route group. null inherits from the uploads block. |
See the Upload modes and HTTP endpoints sections.
Image
Image processing options used by the optimize and variant steps. Do not confuse image.driver (used for variants and optimization) with pdf_preview_engine (used for PDF rasterization).
'image' => [ 'driver' => 'imagick', 'optimize_originals' => false, 'max_width' => 1920, 'max_height' => 1920, 'quality' => 85, ],
driver is imagick (recommended) or gd. When optimize_originals is true, the original is re-encoded to webp within max_width/max_height at quality. In a collection whose image type takes the sphere mode ('image' => ['modes' => ['default', 'sphere']]), DetectSphereStep gives a photo mode sphere when its XMP declares an equirectangular projection (GPano:ProjectionType="equirectangular", as 360 cameras and Google Photo Sphere write it); a wide photo is not a 360 one. See the Images, variants and watermarks section.
PDF preview engine
Selects the engine that rasterizes a PDF page into a PNG for a page variant ('type' => 'image' in the document variants).
'pdf_preview_engine' => 'auto',
| Value | Requirements | Notes |
|---|---|---|
pdftoppm |
poppler-utils (apt install poppler-utils) |
Does not need Ghostscript or any change to ImageMagick policy.xml. |
imagick |
PHP imagick extension, Ghostscript (gs), and the PDF coder enabled in ImageMagick policy.xml |
Heavier setup. |
auto |
tries pdftoppm, falls back to imagick |
Default. |
You can override the engine per variant:
'variants' => ['portada' => ['type' => 'image', 'page' => 1, 'width' => 600, 'engine' => 'pdftoppm']],
See the Video and PDF variants section.
Video
Defaults for ffmpeg-based transcoding ('transcode' => true and the video copies in variants) when a variant does not set its own max_width, max_height or bitrate_kbps.
'video' => [ 'max_width' => 1920, 'max_height' => 1920, 'bitrate_kbps' => 2500, ],
See the Video and PDF variants section.
Encryption
Driver used to encrypt the binary for collections with encrypt: true.
'encryption' => [ 'driver' => 'laravel', ],
laravel uses the framework Crypt facade. See the Sensitive content and encryption section.
Embeddings
Opt-in text extraction and vector embeddings. enabled is the master switch: when false, nothing is embedded even if a collection asks for it.
'embeddings' => [ 'enabled' => false, 'provider' => 'openai', 'api_key' => env('LARACRATE_EMBEDDINGS_API_KEY'), 'model' => env('LARACRATE_EMBEDDINGS_MODEL', 'text-embedding-3-small'), 'dimensions' => 1536, 'chunk_size' => 1000, 'chunk_overlap' => 100, 'batch_size' => 16, 'extractors' => [], 'min_text_per_file' => 100, ],
| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Master switch for the whole feature. |
provider |
openai |
Provider implementing EmbeddingProvider. The package ships an OpenAI provider; the real binding is done in LaracrateServiceProvider. |
api_key |
env LARACRATE_EMBEDDINGS_API_KEY |
If null, the OpenAI provider falls back to OPENAI_API_KEY. |
model |
text-embedding-3-small |
Provider model, overridable per environment. |
dimensions |
1536 |
Vector dimensions. Fixed by the model, change only when you change the model. |
chunk_size |
1000 |
Approximate tokens per chunk. 0 disables chunking (one row per file). |
chunk_overlap |
100 |
Token overlap between consecutive chunks. |
batch_size |
16 |
Chunks per request to the provider. |
extractors |
[] |
Ordered chain of text extractors. Empty means the built-in defaults. |
min_text_per_file |
100 |
Minimum characters an extractor must produce to count as successful. Below this, the next extractor in the chain is tried. |
The extractors chain runs in order; if one returns less than min_text_per_file characters, the next is tried. A typical scanned-PDF chain is PdfTextExtractor (fast, native PDFs) then OcrPdfTextExtractor (LLM OCR) then PlainTextExtractor. See the Text extraction, embeddings and search (RAG) section for the full behavior.
Chunks
Selects the ChunkStore backend that persists and searches text chunks.
'chunks' => [ 'driver' => env('LARACRATE_CHUNKS_DRIVER', 'mysql'), ],
| Driver | Storage | Notes |
|---|---|---|
mysql |
laracrate_file_chunks (SQL LIKE keyword match plus cosine similarity in PHP) |
No external dependencies. Scales well up to roughly 5K chunks per scope. |
meilisearch |
A Meilisearch index with user-provided embeddings | Native hybrid search (BM25 plus vector) with semanticRatio server-side. Requires meilisearch/meilisearch-php and a Meilisearch\Client binding in your app. |
Custom backends (Qdrant, pgvector) can bind ChunkStore directly. See the Text extraction, embeddings and search (RAG) section.
Meilisearch
Applies only when chunks.driver is meilisearch.
'meilisearch' => [ 'index' => env('LARACRATE_MEILISEARCH_INDEX', 'laracrate_file_chunks'), 'embedder' => env('LARACRATE_MEILISEARCH_EMBEDDER', 'default'), ],
OCR
Config for OcrPdfTextExtractor, the fallback for scanned PDFs. The provider is selectable via env, and each provider's API key falls back to the generic key for that provider.
'ocr' => [ 'provider' => env('LARACRATE_OCR_PROVIDER', 'anthropic'), // Fallback language for the auto-generated image description when the // image has no visible text to infer the language from (image OCR only). 'locale' => 'en', 'anthropic' => [ 'api_key' => env('LARACRATE_ANTHROPIC_API_KEY') ?: env('ANTHROPIC_API_KEY'), 'model' => env('LARACRATE_OCR_ANTHROPIC_MODEL', env('LARACRATE_OCR_MODEL', 'claude-haiku-4-5')), ], 'openai' => [ 'api_key' => env('LARACRATE_OPENAI_API_KEY') ?: env('OPENAI_API_KEY'), 'model' => env('LARACRATE_OCR_OPENAI_MODEL', env('LARACRATE_OCR_MODEL', 'gpt-4o-mini')), ], ],
provider is anthropic (default, model claude-haiku-4-5) or openai (model gpt-4o-mini). locale only affects image OCR (OcrImageTextExtractor): the image description follows the visible text's language, falling back to this locale when the image has no text. See the Text extraction section.
Watermark
Settings for the watermark embedded into specific variants. The original (master) is never watermarked; only variants that declare 'watermark' => true are. The defaults here are global, applied wherever a variant opts in.
'watermark' => [ 'image_path' => env('LARACRATE_WATERMARK_IMAGE', null), 'size' => 0.40, 'opacity' => 30, 'position' => 'center', 'text' => [ 'content' => null, 'font_size_ratio' => 0.0195, 'color' => 'rgba(255, 255, 255, 0.60)', 'position' => 'bottom-left', 'padding' => 20, 'font_path' => null, ], ],
| Key | Default | Meaning |
|---|---|---|
image_path |
env LARACRATE_WATERMARK_IMAGE, else null |
Absolute path or path relative to public_path() of the PNG to overlay. null applies no image. |
size |
0.40 |
Watermark width as a fraction of the variant width (0.0 to 1.0). |
opacity |
30 |
Overlay opacity (0 to 100). |
position |
center |
One of center, top-left, top-right, bottom-left, bottom-right. |
text.content |
null |
Optional auxiliary text: null, a fixed string, or a closure(File): ?string for dynamic text (set via a provider or published config, not env). |
text.font_size_ratio |
0.0195 |
Font size as a fraction of the image width. |
text.color |
rgba(255, 255, 255, 0.60) |
CSS rgba color. |
text.position |
bottom-left |
One of bottom-left, bottom-right, top-left, top-right. |
text.padding |
20 |
Padding from the edge, in pixels. |
text.font_path |
null |
Path to a .ttf font, or null for the system font. |
See the Images, variants and watermarks section for the mechanics.
Queue
Routing for the package jobs (variants, previews, embeddings) dispatched by ProcessFileJob.
'queue' => [ 'connection' => env('LARACRATE_QUEUE_CONNECTION', null), 'name' => env('LARACRATE_QUEUE_NAME', 'default'), ],
connection of null uses your default queue connection. All processing runs on the queue by design, so the user's upload stays instant. See the Processing pipeline section.