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
| Piece | Path |
|---|---|
| Models | app/Models/Document.php, DocumentCategory, DocumentFolder, DocumentVersion, DocumentLink |
| Services | app/Services/Tenant/DocumentService.php, DocumentCategoryService.php, DocumentFolderService.php, DocumentVersionService.php, DocumentLinkService.php |
| Upload / quota | app/Services/Storage/FileUploadService.php, WorkspaceStorageService.php |
| Controllers | app/Http/Controllers/Tenant/Api/V1/DocumentController.php, DocumentCategoryController.php, DocumentFolderController.php, DocumentVersionController.php |
| Requests | app/Http/Requests/Tenant/Api/V1/Document/*, DocumentCategory/*, DocumentFolder/* |
| Resources | app/Http/Resources/Tenant/Api/V1/Document/*, DocumentCategory/*, DocumentFolder/* |
| Policies | app/Policies/DocumentPolicy.php, DocumentCategoryPolicy.php, DocumentFolderPolicy.php |
| Events | app/Events/DocumentCreated.php, DocumentUpdated.php, DocumentDeleted.php |
| Subscriber | app/Listeners/DocumentEventSubscriber.php (platform audit) |
| Tests | tests/Feature/Tenant/Document/DocumentTest.php |
Domain notes
- Optional flat
category_idFK (nullOnDelete) plus optional nestedfolder_idFK (nullOnDelete) ondocument_folders(max depth 5; delete blocked with children or documents). - Versions (1.7.0): file replace archives current blob as
document_versionsthen 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_linkspolymorphic pivot (DocumentLink/DocumentLinkService::syncon create + update). Optional arrays:lead_ids,contact_ids,company_ids,project_ids,employee_ids,asset_ids,task_ids. Each id validated with the matchingLinkable*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 chipscontact_id/company_id(AND when both set) for Contact/Company hubs and Documents list deep links. - Categories:
name, autoslug(Str::slugwhen 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::createcallsWorkspaceStorageService::assertCanStorethenFileUploadService::storeunderFileUploadService::tenantDirectory($tenantId, 'documents')(public: false). - Soft delete does not remove objects until force delete or trash retention purge;
forceDeletingdeletes current + version paths viaFileUploadService(covers API +trash:purge-expired). - Ownership:
DocumentPolicy::delete/forceDeleterequire the Spatie permission and (created_by === actoror workspace owner /superadminrole). Bulk endpoints use the same Gate checks per id. - Restore calls
assertCanStorefor currentsize_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}(nameddocuments.update.post); JSON metadata-only updates may usePUT. PHP does not populate multipart files on true HTTP PUT. Document/DocumentCategory/DocumentFolderare registered inTrashPurgeRegistry(workspacetrash.retention_days).WorkspaceStorageService::usedBytes()sums non-trasheddocuments.size_bytesplus theirdocument_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.deleteis 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.deleteRoutes use module:documents then can:documents.* / policies. Category CRUD reuses the same documents.* permissions (no separate category permission family).
Catalog
- Slug
documents, categoryoperations,sort_order85, iconfile-text is_default_included = false,is_billable = false, version 1.7.0- Hard
module_dependenciesrow: 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
| Concern | Behavior |
|---|---|
| Install gate | ModuleSubscriptionService rejects Documents without entitled Storage |
| Upload gate | assertCanStore before store / replace / version restore |
| Used bytes | Active document + version size_bytes included in quota |
| Soft delete | Excluded from usedBytes; objects remain until force delete or trash purge |
| Force delete / trash purge | Current + version objects removed from disk in forceDeleting |
API (tenant)
Base: /api/tenant/v1 — full reference tenant-v1-documents.md.
| Method | Path | Permission |
|---|---|---|
| GET | /documents | view |
| GET | /documents/stats | view |
| GET | /documents/folders | view |
| GET | /documents/folders/{documentFolder} | view |
| POST | /documents/folders | create |
| PUT | /documents/folders/{documentFolder} | update |
| DELETE | /documents/folders/{documentFolder} | delete |
| POST | /documents/folders/{documentFolder}/restore | restore |
| DELETE | /documents/folders/{documentFolder}/force | force.delete |
| GET | /documents/{document} | view |
| GET | /documents/{document}/download | view |
| GET | /documents/{document}/versions | view |
| GET | /documents/{document}/versions/{version}/download | view |
| POST | /documents/{document}/versions/{version}/restore | update |
| POST | /documents | create |
| PUT | /documents/{document} | update |
| DELETE | /documents/{document} | delete |
| POST | /documents/{document}/restore | restore |
| DELETE | /documents/{document}/force | force.delete |
| GET | /document-categories | view |
| GET | /document-categories/{documentCategory} | view |
| POST | /document-categories | create |
| PUT | /document-categories/{documentCategory} | update |
| DELETE | /document-categories/{documentCategory} | delete |
| POST | /document-categories/{documentCategory}/restore | restore |
| DELETE | /document-categories/{documentCategory}/force | force.delete |
Frontend
| Piece | Path |
|---|---|
| Page | src/pages/documents/documents-page.tsx |
| Form | document-form.tsx, document-form-page.tsx (optional related-record pickers when entitled modules + view permissions are present) |
| View | document-view-page.tsx (Related records cards from links) |
| Service | documentService / documentCategoryService in src/api/services.ts |
| Nav | permission: documents.view, module: 'documents' |
Tests
bash
# Backend
php artisan test --compact --filter=Document
# Frontend
npm run test:e2e:documents