← Laracrate 4 / 19

Core concepts

Before you reach for the reference sections, here is the vocabulary Laracrate uses. Every file is a row in laracrate_files (an Eloquent EduLazaro\Laracrate\Models\File) decorated with the concepts below. Read this once and the rest of the docs will click.

Collection. The business grouping a file belongs to, stored in the collection column (for example avatar, documents, lawsuit-document). A collection is declared in config('laracrate.collections.*') and decides the disk, the access (public or private), the accepted types, the variants to generate, and whether text extraction or embeddings run. When you attach a file to one of your models you always name its collection (see the Working with files from your models section).

Type. What a file is, stored in the type column and cast to the EduLazaro\Laracrate\Enums\FileType enum: IMAGE, VIDEO, AUDIO, DOCUMENT. Laracrate derives it from the MIME with FileType::fromMime() (anything that is not image/*, video/*, or audio/* becomes DOCUMENT); the format itself stays in mime_type and extension, and the pipeline picks its steps by the type.

Mode. How an image is shown, in the mode column, cast to EduLazaro\Laracrate\Enums\FileMode: default, sphere (a 360° photo) or frames (a strip of a video's frames). Its type stays image, so it is processed, listed and served as any other image.

File vs variant. A top-level file has parent_id = null. A variant is a derived file (a thumbnail, an MP4 copy, a video frame, a PDF page) linked to its parent by parent_id and named by the variant column with the name your config gives it. Variants are real File rows, created with $file->createVariant($name, $overrides), and they inherit the parent scope (fileable, creator, tenant, disk, collection, access). You navigate them with dot notation: $file->variant('portada.mini') walks the tree and falls back to the nearest existing ancestor instead of returning null, while $file->variantOrFail('portada.mini') throws if any link is missing. $file->preview() is the variant the collection names as the type's preview. Helpers: $file->isTopLevel(), $file->isVariant(). See the Images, variants and watermarks and Video and PDF variants sections.

The four polymorphic morphs. A file carries four independent morphTo relations. They are orthogonal, not a hierarchy, and you set only the ones your app needs.

Morph Columns Meaning
fileable fileable_type, fileable_id What the file belongs to (a Property, User, Service).
creator creator_type, creator_id Who created it. Null for system-generated files.
owner owner_type, owner_id The semantic owner when it differs from the creator (uploaded on behalf of someone). Null when it matches the creator.
tenant tenant_type, tenant_id The multi-tenant scope. Leave null in single-tenant apps.

Use $file->effectiveOwner() to get the explicit owner or fall back to the creator. See the Multi-tenancy, buckets and usage section.

path is the full object key. The path column stores the complete object key on the disk (directories, filename, and extension together), for example user/1/avatar/01J...webp. It is not just the directory, and you never concatenate it with name to build a key. Always read the key through the accessor $file->key, which trims a stray leading slash. Derived keys come from $file->siblingKey($name) (same directory) and $file->variantKey($name) (sibling variants/ subdirectory). The name column is just basename($path).

Access. The access column, cast to EduLazaro\Laracrate\Enums\FileAccess, says who a file is for:

Case Meaning
PUBLIC Anyone. url() is its permanent public URL (Storage::url()), with no signature and no audit.
PRIVATE Only who its view policy lets in. url() signs a link straight to the disk that expires; streamUrl() and downloadUrl() serve it through PHP, checking the policy on every request.

How a private file is served is chosen where it is used, not per collection. The exception is a file marked sensitive or stored encrypted: it never gets a direct link, so url() returns its stream route. Each collection declares its access. See the Displaying files and Access control and authorization sections.

Visibility. A separate axis from access, stored in the visibility column and cast to EduLazaro\Laracrate\Enums\FileVisibility: OWNER, GROUP, TENANT, or WORLD. Where access governs the transport, visibility expresses the intended audience for your policies to enforce.

Processing status. The lifecycle of the async pipeline, in the processing_status column and cast to EduLazaro\Laracrate\Enums\ProcessingStatus: PENDING (just created, queued) goes to PROCESSING (steps running) and ends in either COMPLETED (all applicable steps ran) or FAILED (a step threw, with the message in processing_error). The enum offers isTerminal() and isInProgress(). This applies only to top-level files; variants are born COMPLETED. See the Processing pipeline section.

Chunk. A piece of extracted text, modeled by EduLazaro\Laracrate\Models\FileChunk and accessed via $file->chunks() (ordered by chunk_index) or $file->chunk() (the first chunk). Each chunk carries chunk_index, text, tokens, metadata, and an optional context. The lightweight chunk row lives in laracrate_file_chunks; the heavy payload (text plus embedding) lives in laracrate_file_chunk_data. Chunks are the unit of RAG search. See the Text extraction, embeddings and search section.

ChunkStore. The contract (EduLazaro\Laracrate\Contracts\ChunkStore) that abstracts where chunks are persisted and searched. Its methods are store(File $file, array $chunks): int, getByFile(File $file): Collection, search(string $query, array $filters = [], array $options = []): Collection, deleteByFile(File $file): void, and driverName(): string. The shipped drivers are MysqlChunkStore (LIKE plus cosine similarity in PHP) and MeilisearchChunkStore (native hybrid BM25 plus vector). Bind your own implementation (Qdrant, pgvector) to swap backends.

Embedding. A numeric vector that captures the meaning of a chunk (1536 dimensions for text-embedding-3-small), produced by an EmbeddingProvider and used for semantic similarity. Use $file->hasEmbeddings() to check whether every chunk has one.

semantic_ratio. The 0-1 weight of semantic ranking versus keyword ranking in ChunkStore::search(), passed in the options array (default 0.7). A value of 0 is keyword only and embeds nothing (no embedding API cost), 1 is fully semantic, and anything above 0 embeds the query.

Slot. A named placeholder a user fills in (for example "ID front", "ID back"), modeled by EduLazaro\Laracrate\Models\FileSlot with extension and type restrictions and quotas. Files attach to slots through the laracrate_file_slot_pivot table, reachable via $file->slots(). See the File slots section.

Folder. An optional logical grouping under a fileable, modeled by EduLazaro\Laracrate\Models\Folder and referenced by the folder_id column (null means the fileable root). Move a file with $file->moveToFolder($folder); the binary key on the backend never changes, only folder_id. See the Folders section.

Multipart. The protocol for large uploads (at or above the configured multipart.threshold, default 100 MB). The binary is split into parts of at least 5 MB, uploaded in parallel with per-part retries and reassembled by ETags. The session is tracked in laracrate_multipart_uploads. See the Upload modes section.

Presigned URL. A cryptographically signed, time-limited URL that authorizes a direct operation against the storage backend (PUT to upload, GET to download) without exposing your credentials. The browser uploads straight to R2 or S3 and your application server never touches the bytes. See the Upload modes and HTTP endpoints sections.

Data model

Laracrate ships eight tables, all prefixed with laracrate_. The prefix avoids collisions with the legacy files table that already exists in many Laravel apps. The class names do not repeat the prefix (File, not LaracrateFile), because the EduLazaro\Laracrate\Models namespace already disambiguates. This follows the Cashier and Media Library convention.

The schema started with three tables and grew. Two renames matter if you are upgrading: laracrate_file_contents was renamed to laracrate_file_chunks, and an intermediate laracrate_file_chunk_data table existed for a few migrations before being folded back into laracrate_file_chunks (its text and embedding columns now live directly on the chunk row). The migrations are idempotent, so a fresh install lands on the final shape below.

Tables

Table Model Purpose
laracrate_files File One row per file (and per variant). The central table.
laracrate_file_chunks FileChunk Extracted text split into chunks, with embeddings for search. One row per chunk.
laracrate_multipart_uploads MultipartUpload Active and historical S3/R2 multipart upload sessions.
laracrate_file_slots FileSlot Named upload slots with per-slot rules (allowed types, count limits).
laracrate_file_slot_pivot (pivot, via File::slots()) Many-to-many link between files and slots.
laracrate_tenant_buckets TenantBucket Per-tenant dedicated bucket overrides for a base disk.
laracrate_folders Folder Folder tree (parent/child plus denormalized path) for organizing files.
laracrate_folderables Folderable Aggregated storage usage counter per (owner, collection).

The laracrate_files.folder_id column links a file to a folder. There is no separate file/folder pivot: a file belongs to at most one folder.

laracrate_files

The File model (EduLazaro\Laracrate\Models\File) wraps this table. Its route key is slug. It uses soft deletes. Columns, grouped by concern:

Identity and hierarchy

Column Type Notes
id bigint Primary key.
slug ulid Unique. Used as the public route key (never expose id).
parent_id bigint, nullable Points at the parent file. Null means top-level. Set means this row is a variant. Cascades on delete.
variant string(50), nullable Variant name, as the collection config names it (thumbnail, web, portada, ...). Unique together with parent_id.
folder_id bigint, nullable Folder this file lives in. Null means the root of its fileable. Nulls on folder delete.

The four morphs

Each file carries four independent polymorphic relations (see the Core concepts section for why they stay orthogonal):

Columns Relation Meaning
fileable_type, fileable_id fileable() What the file belongs to (a User, Property, Service).
creator_type, creator_id creator() Who or what created the row. Null for system-generated.
owner_type, owner_id owner() Semantic owner when it differs from the creator. Falls back to the creator via effectiveOwner().
tenant_type, tenant_id tenant() Multi-tenant scope (Organization, Workspace). Null for single-tenant apps.

Storage key

Column Type Notes
disk string The Laravel disk name, or a tb:{id} token for a dedicated tenant bucket.
path string The full object key in the disk (directories, filename, extension). Read it through $file->key, never by hand.
name string basename($path) denormalized.
original_name string The filename as uploaded.
extension string(10) Lowercase extension.
mime_type string(100) Detected MIME type.
size unsignedBigInteger Bytes.
digest string(80), nullable Content hash for dedupe or integrity.

Classification

Column Type Notes
context string Defaults to laracrate.default_context. Indexed.
collection string Defaults to laracrate.default_collection. Indexed.
type enum image, video, audio, document. Cast to FileType. Indexed.
mode string How an image is shown: default, sphere or frames (since migration 0000_00_00_000042). Cast to FileMode. Indexed.
category string, nullable Free-form app category. Indexed.

Access flags

Column Type Notes
access enum public or private. Defaults to private. Cast to FileAccess through AsFileAccess, which reads the 1.x values signed and stream as private until migration 0000_00_00_000041 rewrites them (a stream row also becomes sensitive). Indexed.
visibility string, nullable Free-form visibility label. The FileVisibility enum (owner, group, tenant, world) is available if you want to use its values. Indexed.
sensitive boolean Defaults to false. A sensitive file never gets a direct link: it is only served through PHP. Indexed.
is_encrypted boolean Defaults to false.

Metadata and presentation

Column Type Notes
title string, nullable Display title.
description text, nullable Display description.
label string(100), nullable Short label.
default boolean Marks the default file in its (fileable + collection) group.
position unsignedInteger Sort order. Defaults to 0.
published boolean Defaults to true. Indexed.
is_verified boolean Defaults to false. Indexed.
metadata json, nullable Free-form bag. Cast to array.

Media metadata

Column Type Notes
duration unsignedInteger, nullable Seconds, for video and audio.
width, height unsignedInteger, nullable Pixels, for image and video.
bitrate unsignedInteger, nullable For video and audio.
sample_rate unsignedInteger, nullable For audio.

Processing and audit

Column Type Notes
processing_status enum, nullable pending, processing, completed, failed. Cast to ProcessingStatus.
processing_error text, nullable Error message when a step throws.
processing_started_at timestamp, nullable When the pipeline began.
processing_extractor string(255), nullable Class of the text extractor used.
processing_provider string(50), nullable Embedding or extraction provider.
processing_model string(100), nullable Model used for embedding or extraction.
summary text, nullable Optional LLM-distilled summary of the extracted content.
downloads_count unsignedInteger Defaults to 0.
last_downloaded_at timestamp, nullable Updated on each served download.

Indexing trackers

These three timestamps record where a file's chunks have been indexed, so you can re-index incrementally and migrate between search backends safely (see the Text extraction, embeddings and search section).

Column Type Notes
mysql_indexed_at timestamp, nullable Chunks ready in MySQL (LIKE keyword plus cosine in PHP). Indexed.
meili_indexed_at timestamp, nullable Chunks pushed to Meilisearch. Indexed.
storage_indexed_at timestamp, nullable .chunks.jsonl backup written.

Timestamps: created_at, updated_at, plus deleted_at (soft deletes).

laracrate_file_chunks

Wrapped by FileChunk. One row per chunk of extracted text. A collection with no chunking stores everything in a single row at chunk_index 0. The foreign key to laracrate_files cascades on delete.

Column Type Notes
id bigint Primary key.
file_id bigint Parent file. Cascades on delete.
chunk_index unsignedInteger Position within the file. Defaults to 0. Unique together with file_id.
context string(30), nullable Discriminator for multi-section extractions (text for OCR verbatim, description for a generated visual description, or any opaque label). Indexed with file_id.
text longText, nullable Chunk text. The column has a FULLTEXT index, though the MySQL chunk store currently keyword-matches with SQL LIKE.
embedding json, nullable Embedding vector. Cast to array. Cosine similarity is computed in PHP.
tokens unsignedInteger, nullable Token count for the chunk.
metadata json, nullable Free-form bag (page numbers, etc). Cast to array.

File::chunks() returns these ordered by chunk_index, and File::chunk() returns the single chunk_index 0 row. The older contents() and content() relations are kept as deprecated aliases for apps migrating from the previous table name.

laracrate_multipart_uploads

Wrapped by MultipartUpload. One row per multipart upload session against an S3-compatible disk. Small files use a single PUT and never touch this table. Completed and aborted rows are kept as an audit trail rather than deleted (see the Upload modes section).

Key columns: upload_id (unique, the provider's id), disk, key, mime_type, expected_size, part_size, total_parts, and status (enum active, completed, aborted, expired, cast to MultipartUploadStatus). It mirrors the file morphs with creator_*, tenant_*, and fileable_* columns plus collection, and links to the resulting row via file_id (nulls on file delete). Lifecycle timestamps: expires_at, completed_at, aborted_at, plus an error column.

laracrate_file_slots and laracrate_file_slot_pivot

FileSlot wraps laracrate_file_slots: named upload slots with rules (see the File slots section). Columns: tenant_type/tenant_id and context_type/context_id for scoping, name, description, color, allowed_extensions (json array), allowed_types (json array of FileType values), max_files_per_creator, max_files_total, and position.

laracrate_file_slot_pivot is the many-to-many link, with file_id and file_slot_id (both cascade on delete, unique together). It has no dedicated model: reach it through File::slots() or FileSlot::files().

laracrate_tenant_buckets

TenantBucket wraps this table: one row overrides a single config disk (the base_disk) with a dedicated bucket for one tenant (see the Multi-tenancy, buckets and usage section). Columns: tenant_type/tenant_id, base_disk, bucket, public_url (nullable), credentials (longText, cast to encrypted:array for bring-your-own-account setups), is_active, and label. Unique on (tenant_type, tenant_id, base_disk). toDiskConfig() merges the base disk config with the override.

laracrate_folders

Folder wraps this table: a parent/child folder tree with a denormalized path kept in sync by an observer (see the Folders section). It uses soft deletes. Columns: a folderable_* morph (the tree owner), parent_id (cascades on delete, null means root), name, path (string(500)), a creator_* morph, and a metadata json bag. Unique on (folderable_type, folderable_id, path).

laracrate_folderables

Despite the name, this is not a pivot. Folderable wraps an aggregated usage counter, one row per (folderable_type, folderable_id, collection), maintained in real time by the file observer when a collection has track_usage enabled. Columns: the folderable_* morph, collection, total_size_bytes, files_count, folders_count, and last_recomputed_at. Unique on (folderable_type, folderable_id, collection). The laracrate:recompute-usage command rebuilds it if you suspect drift.