Skip to content

Documents — Developer Guide ​

Mirror of the Assets developer guide and Knowledge Base flat-category patterns. Prefer copying those patterns over inventing new ones. Uploads reuse FileUploadService; quota uses WorkspaceStorageService.

Backend layout ​

PiecePath
Modelsapp/Models/Document.php, DocumentCategory, DocumentFolder, DocumentVersion, DocumentLink
Servicesapp/Services/Tenant/DocumentService.php, DocumentCategoryService.php, DocumentFolderService.php, DocumentVersionService.php, DocumentLinkService.php
Upload / quotaapp/Services/Storage/FileUploadService.php, WorkspaceStorageService.php
Controllersapp/Http/Controllers/Tenant/Api/V1/DocumentController.php, DocumentCategoryController.php, DocumentFolderController.php, DocumentVersionController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Document/*, DocumentCategory/*, DocumentFolder/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Document/*, DocumentCategory/*, DocumentFolder/*
Policiesapp/Policies/DocumentPolicy.php, DocumentCategoryPolicy.php, DocumentFolderPolicy.php
Eventsapp/Events/DocumentCreated.php, DocumentUpdated.php, DocumentDeleted.php
Subscriberapp/Listeners/DocumentEventSubscriber.php (platform audit)
Teststests/Feature/Tenant/Document/DocumentTest.php

Domain notes ​

  • Optional flat category_id FK (nullOnDelete) plus optional nested folder_id FK (nullOnDelete) on document_folders (max depth 5; delete blocked with children or documents).
  • Versions (1.7.0): file replace archives current blob as document_versions then stores the new file (old object kept). Restore copies the version blob to a new path, archives current, and asserts quota for the restored size. Force-delete removes current + all version blobs.
  • Soft record links via document_links polymorphic pivot (DocumentLink / DocumentLinkService::sync on create + update). Optional arrays: lead_ids, contact_ids, company_ids, project_ids, employee_ids, asset_ids, task_ids. Each id validated with the matching Linkable* rule (module entitlement + assignee / visibility scope). Omitted keys leave that link type unchanged; an empty array clears links for that type.
  • Reverse list filter (1.6.0): GET /documents?linkable_type=contact&linkable_id= (and peer types) returns documents linked to that record. Prefer party chips contact_id / company_id (AND when both set) for Contact/Company hubs and Documents list deep links.
  • Categories: name, auto slug (Str::slug when omitted), sort_order, is_active. Soft/force delete blocked while documents still reference the category.
  • Create requires multipart file (max 51200 KB / 50 MB). DocumentService::create calls WorkspaceStorageService::assertCanStore then FileUploadService::store under FileUploadService::tenantDirectory($tenantId, 'documents') (public: false).
  • Soft delete does not remove objects until force delete or trash retention purge; forceDeleting deletes current + version paths via FileUploadService (covers API + trash:purge-expired).
  • Ownership: DocumentPolicy::delete / forceDelete require the Spatie permission and (created_by === actor or workspace owner / superadmin role). Bulk endpoints use the same Gate checks per id.
  • Restore calls assertCanStore for current size_bytes + sum of version bytes before un-trashed bytes count again.
  • Bulk: DocumentService::bulkDelete / bulkForceDelete (max 100 ids) with per-id failure reporting; force requires soft-deleted rows (single + bulk).
  • File replace: prefer POST /documents/{id} (named documents.update.post); JSON metadata-only updates may use PUT. PHP does not populate multipart files on true HTTP PUT.
  • Document / DocumentCategory / DocumentFolder are registered in TrashPurgeRegistry (workspace trash.retention_days).
  • WorkspaceStorageService::usedBytes() sums non-trashed documents.size_bytes plus their document_versions.size_bytes, with chat/feedback/lead-import usage.
  • Spatie activity log name documents / document-categories / document-folders (path/disk excluded from document attribute logs).
  • Platform audit events: document_created, document_updated, document_deleted, document_restored, document_force_deleted.
  • documents.force.delete is not granted to default roles — owner/superadmin only, matching Vendors / Assets.
  • Production readiness: Documents production readiness · folders + versions 1.7.0.
  • Catalog version 1.7.0

Permissions ​

config/tenant-permissions.php:

documents.view | create | update | delete | restore | force.delete

Routes use module:documents then can:documents.* / policies. Category CRUD reuses the same documents.* permissions (no separate category permission family).

Catalog ​

  • Slug documents, category operations, sort_order 85, icon file-text
  • is_default_included = false, is_billable = false, version 1.7.0
  • Hard module_dependencies row: documents → storage (is_optional = false)
  • Registered via migrate-only DefaultModuleRegistrar::ensureModule (no seeder in production)

Making billable later ​

Flip catalog flags with the existing Central Update Module API (Central → Modules → Documents): set is_billable = true and monthly/yearly prices. Do not invent a new Central UI surface.

Storage integration ​

ConcernBehavior
Install gateModuleSubscriptionService rejects Documents without entitled Storage
Upload gateassertCanStore before store / replace / version restore
Used bytesActive document + version size_bytes included in quota
Soft deleteExcluded from usedBytes; objects remain until force delete or trash purge
Force delete / trash purgeCurrent + version objects removed from disk in forceDeleting

API (tenant) ​

Base: /api/tenant/v1 — full reference tenant-v1-documents.md.

MethodPathPermission
GET/documentsview
GET/documents/statsview
GET/documents/foldersview
GET/documents/folders/{documentFolder}view
POST/documents/folderscreate
PUT/documents/folders/{documentFolder}update
DELETE/documents/folders/{documentFolder}delete
POST/documents/folders/{documentFolder}/restorerestore
DELETE/documents/folders/{documentFolder}/forceforce.delete
GET/documents/{document}view
GET/documents/{document}/downloadview
GET/documents/{document}/versionsview
GET/documents/{document}/versions/{version}/downloadview
POST/documents/{document}/versions/{version}/restoreupdate
POST/documentscreate
PUT/documents/{document}update
DELETE/documents/{document}delete
POST/documents/{document}/restorerestore
DELETE/documents/{document}/forceforce.delete
GET/document-categoriesview
GET/document-categories/{documentCategory}view
POST/document-categoriescreate
PUT/document-categories/{documentCategory}update
DELETE/document-categories/{documentCategory}delete
POST/document-categories/{documentCategory}/restorerestore
DELETE/document-categories/{documentCategory}/forceforce.delete

Frontend ​

PiecePath
Pagesrc/pages/documents/documents-page.tsx
Formdocument-form.tsx, document-form-page.tsx (optional related-record pickers when entitled modules + view permissions are present)
Viewdocument-view-page.tsx (Related records cards from links)
ServicedocumentService / documentCategoryService in src/api/services.ts
Navpermission: documents.view, module: 'documents'

Tests ​

bash
# Backend
php artisan test --compact --filter=Document

# Frontend
npm run test:e2e:documents

Official documentation for the EloSync SaaS Platform.