File Storage
Reference
Public
Research Evidence
The live-file evidence this ERPat File Storage guide is built on — the manifest, config sidecars, four migrations, controllers, service libraries, cron jobs, and passing module tests, each with what it confirms.
Guide version: r1
Module version: 1.0.0 Updated: 2026-07-30 Estimated time: 6 min 3 views
Why this page exists
This guide describes behavior, not aspiration. Every claim elsewhere in the documentation traces back to a source file in the shipped module. This page is the audit trail for maintainers and reviewers: it lists the evidence, states what each item proves, and flags the one place where a stated capability is a future integration rather than a shipped feature.
Grounding rule: if it is not in the tables below, it is not asserted in the guide. The scanner hook is the deliberate exception — see
Scanner status.
Manifest — module.json
| Evidence | What it confirms |
Manifest slug: file_storage | The module key is file_storage; the enable setting is module_file_storage; controllers gate with with_module("file_storage") and the left menu appears under "Storage". |
permissions[] list | The full permission surface: base file_storage (+ _create/_update/_delete), sharing (_share_internal, _share_external, _manage_permissions, _create_space, _manage_space, _create_file_request), and the high-risk governance set (_view_activity, _manage_quarantine, _manage_policies, _legal_hold, _purge). |
api / feature declarations | The module ships BOTH API surfaces — a client Integration API (/api/v1/file-storage/*) and an End-User API (/v1/api/file-storage/*) — plus cron jobs, widgets, and seeders. |
No tenant_id anywhere in the schema declarations | Multi-tenancy is achieved by tenant-DB isolation, not a tenant column — consistent with ERPat's database-per-tenant model. |
Config sidecars — config/*.php
| Evidence | What it confirms |
config/module_config.php | module_key equals the manifest slug (file_storage) and the module registers under the "Storage" category in Manage Modules. |
config/permissions.php | The permission keys are owned by the module (not hardcoded in core Roles.php) and appear in the Roles editor only while the module is enabled — including the governance keys as a distinct high-risk group. |
config/menu.php + config/default_menu.php | The single tabbed workspace lands under "Storage" in the live sidebar, and the default-menu slice mirrors it (names in lockstep) so it survives "Restore to default". |
config/routes.php | All module routes — the app workspace, the governance console, the public share pages, and both API surfaces — are declared in the module, never in core. |
config/api_routes.php + config/euapi_routes.php | The client API mounts at api/v1/file-storage and the end-user API at v1/api/file-storage; both are globbed into the OpenAPI generators. |
config/api_scope.php | Two client scopes exist and are gated per endpoint: filestorage:read and filestorage:write. |
config/widgets.php (+ widget helper) | Two dashboard widgets are registered and gated on the module setting: "My Recent Files" and "Storage Usage". |
Migrations — the four schema layers (no tenant_id)
The schema is delivered in four idempotent, reversible migrations that map one-to-one to the release plan. None of the tables carry a tenant_id column — isolation is by tenant database.
| Migration (evidence) | Tables | What it confirms |
| Foundation (R1) |
file_spaces, file_nodes, file_versions, general_file_references, file_acl_entries, file_upload_sessions, file_activity_events, file_favorites |
Folders and logical files (file_nodes), immutable versions pointing back to general_files (file_versions), the reference ledger that makes purge reference-aware (general_file_references), the role-based ACL (file_acl_entries), validated-intake sessions (file_upload_sessions), the activity feed (file_activity_events), and starring (file_favorites). |
| Collaboration (R2) |
file_comments |
Comments on files, plus the space-membership and Shared-with-Me grant plumbing that the collaboration release adds on top of the foundation tables. |
| Sharing (R3) |
file_share_links |
External share links store only the SHA-256 token hash (never the raw token), the scope, expiry, max-opens counter, Argon2id password hash, OTP settings, and the download-allowed flag — the evidence that raw secrets are not persisted. |
| Governance (R4) |
file_legal_holds, file_legal_hold_items |
Legal holds and their held-item links exist as first-class tables, enabling fail-closed blocking of permanent purge. |
Every table ends with the standard audit block and a soft-delete flag; migrations guard schema changes (SHOW COLUMNS/SHOW INDEX) and ship a reversible down(), so they re-run safely on already-provisioned tenant databases.
Controllers — controllers/*.php
| Controller (evidence) | Base & gates | What it confirms |
FileStorage | App_Controller; with_module + with_permission | The tabbed workspace (My Files, Shared with Me, Recent, Starred, Trash, Activity) as one lazy-tab page, plus spaces() — the dedicated Team Spaces page (space rail + the active space’s directory, deep-linkable to a space and folder). |
FileStorage_admin | App_Controller; governance permissions | The separate admin console with Policies, Quarantine, Legal Holds, and Risk tabs — distinct from the everyday workspace. |
FileShare | GuestController | Public, unauthenticated share landing: token resolution, password/OTP gates, download, and upload-only file requests — accessible without a session. |
FileStorage_api | ApiAuthController; authorizeScope | The client Integration API gates each action on filestorage:read / filestorage:write. |
FileStorage_euapi | EndUserApiAuthController; requireModule + requirePermit + selfUserId() | The end-user API is strictly self-scoped — identity comes only from selfUserId(), never a caller-supplied id. |
Service libraries — libraries/*.php
Business logic lives in eight single-purpose services, not in the controllers — the evidence that the module keeps SQL and rules out of the request handlers.
| Service (evidence) | What it confirms |
File_registry_service | Files are recorded against ERPat's shared general_files registry as owner-less rows and reconciled from it — the mechanism behind isolation-by-construction. |
File_node_service | Folder/file node CRUD: create, rename, move, star, trash/restore within the logical tree. |
File_upload_service | Validated intake reusing ERPat's existing extension/MIME/magic-byte checks and local/S3 pipeline; no separate uploader. |
File_lifecycle_service | Versioning, soft-delete-first trashing, restore, and reference-aware safe purge. |
File_authorization_service | The server-side ACL: roles (viewer/commenter/contributor/editor/manager/owner/upload_only) with folder + space inheritance, computed server-side. |
File_share_service | External link issuance — token hashing, scope, expiry, atomic max-opens, Argon2id password, and email OTP. |
File_scanner_service | A pluggable scanner HOOK exists (see the scanner note); it is the seam where real AV/CDR would attach. |
File_purpose_policy | The purpose-policy registry that Governance → Policies edits. |
Cron jobs — jobs/*.php
Six Advanced Cron jobs run per-tenant and each self-gates on module_file_storage — verified by their declared slugs and schedules.
| Slug (evidence) | Schedule | What it confirms |
file_storage_upload_pipeline | every 1 min | Uploaded bytes are processed asynchronously through the intake pipeline. |
file_storage_access_expirer | every 5 min | Expired share links and grants are deactivated on a schedule. |
file_storage_membership_sync | daily | Team Space membership is kept in sync with department membership. |
file_storage_registry_reconcile | hourly | The reference ledger is reconciled against the shared general_files registry. |
file_storage_safe_purge | daily | Permanent purge runs only after the reference-aware, fail-closed checks pass. |
file_storage_risk_scan | daily | The governance risk dashboard is refreshed on a schedule. |
Module tests — php erpat module:test FileStorage
| Evidence | What it confirms |
The module's tests/ suite passes | The manifest, config sidecars, route targets, permission ownership, and service contracts described above are structurally valid — the suite runs green under php erpat module:test FileStorage (not the core test:run suite). |
| Authorization / safe-purge assertions | The ACL computes as documented and purge is blocked while a reference, current version, retention window, or legal hold is active — the fail-closed guarantees are exercised, not just asserted in prose. |
Run it yourself: php erpat module:test FileStorage — this is the authoritative check that the module still matches this documentation.
Scanner status — the one deliberate gap
Upload validation today reuses ERPat's existing extension, MIME, and magic-byte checks. File_scanner_service is a pluggable hook only — a real anti-virus / content-disarm engine is a FUTURE integration and is not enabled in the shipped module. Do not document AV scanning as a live feature.