Core concepts
Core concepts
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 mode, 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. A coarse media class stored in the type column and cast to the EduLazaro\Laracrate\Enums\FileType enum: IMAGE, VIDEO, AUDIO, or DOCUMENT. Laracrate derives it from the MIME with FileType::fromMime() (anything that is not image/*, video/*, or audio/* becomes DOCUMENT). The pipeline picks which steps run per type.
File vs variant. A top-level file has parent_id = null. A variant is a derived file (a thumbnail, an optimized copy, a video or PDF preview) linked to its parent by parent_id and named by the variant column. 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('preview.thumbnail') walks the tree and falls back to the nearest existing ancestor instead of returning null, while $file->variantOrFail('preview.thumbnail') throws if any link is missing. Helpers: $file->isTopLevel(), $file->isVariant(). See the Images, variants and watermarks section.
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 mode. The access column, cast to EduLazaro\Laracrate\Enums\FileAccess, decides how a file is served:
| Case | How it is served |
|---|---|
PUBLIC |
Direct CDN URL via Storage::url(). No signature, no audit. |
SIGNED |
Temporary signed URL via Storage::temporaryUrl(), cached server-side. |
STREAM |
Served through the package controller: per-request authorization, audit, optional viewer binding, encryption, and watermark. |
Each collection declares its access mode. See the Upload modes 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 (thumbnail, preview, small, ...). 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. |
category |
string, nullable | Free-form app category. Indexed. |
Access flags
| Column | Type | Notes |
|---|---|---|
access |
enum | public, signed, stream. Defaults to signed. Cast to FileAccess. 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. 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.