
Keep a durable record of every stored file
A file needs more than a URL. Wildo keeps a record of its type, size, storage destination, owner and lifecycle state, while the bytes live in the selected storage provider.
That shared record lets uploading, serving, scanning and cleanup work with the same file identity. Application records hold lightweight references instead of copying storage details into every attachment field.
Example — Replace an attachment without losing its lifecycle
A contract record receives a new version of a document. Its file reference changes, while the old file row can move into the configured removal lifecycle. The storage address and cleanup state remain available independently of the contract’s current value.
For engineers
Keep content metadata separate from byte routing
The internal file schema records the original names, content facts and provider handle. In files.shared.schemas.ts, the following fields sit in one FileSchemaBase; comments and the later lifecycle fields are omitted.
fileId: z.string().min(1).isPrimaryKey().isSummaryField(),
filename: z.string().min(1).isDBIndexed().isSummaryField().dataCategories(PersonalDataCategory.USER_CONTENT),
originalFilename: z.string().min(1).isSummaryField().dataCategories(PersonalDataCategory.USER_CONTENT),
mimeType: z.string().min(1).isDBIndexed().isSummaryField(),
size: z.number().min(0).isSummaryField(),
extension: z.string().optional(),
checksum: z.string().optional().isDBIndexed(),
storageProvider: z.enum(FileStorageAccepted).optional().isDBIndexed(),
storageRef: z.string().min(1).optional().isDBIndexed(),
Use file services for lifecycle writes
The FILES resource supplies internal repository operations. File services create and update its rows; purpose-specific file routes expose authorized downloads, metadata, sharing and deletion. This does not publish a generic file-management CRUD interface just because a schema exists.
Creation starts with a pending row. Upload completion, scanning, linking, unlinking and deletion each have defined state transitions. Parent linking fills the resource type, record ID and canonical field name; scope is already derived during resource upload and is checked again when linking.
Do not substitute metadata for the storage handle
storageProvider and storageRef determine which backend and object to read. storageKey is scoped metadata and can be derived again as ownership is finalized; it is not a replacement for the persisted backend handle. Changing a field’s future storage preference does not move existing bytes.
Uploader attribution is an optional ID-and-type pair, and media analysis fields are populated only when the corresponding processing succeeds. Do not infer image dimensions, an uploader or a preview URL merely from the existence of a file row. Read through the file service so scope and status rules apply.
Compare the three representations of one attachment
Consider a saved request with one attachment. Its parent value needs only the reference and field timestamp; it does not need storage credentials, provider addresses or lifecycle internals. This is an illustrative JSON representation of that parent field:
{
"attachment": {
"fileId": "507f1f77bcf86cd799439011",
"updatedAt": "2026-09-12T10:00:00.000Z"
}
}
The internal file row and the authorized metadata response answer different questions about that same identifier:
| Information | Internal file row | Public metadata projection |
|---|---|---|
| Identity | fileId: "507f1f77bcf86cd799439011" | The same fileId. |
| Content | Original filename, MIME type and byte size. | Display filename, MIME type and size. |
| Lifecycle | status: FileStatus.LINKED after attachment. | readiness: FileReadiness.ATTACHED. |
| Parent binding | linkedResourceType, linkedResourceId, linkedFieldName. | These binding fields are not copied into the metadata projection. |
| Storage | storageProvider and the persisted storageRef. | The raw provider handle is not exposed. |
| Ownership | Application, organization or user scope. | Access is checked before the authorized route returns metadata. |
toMetadataResponse translates internal lifecycle into the smaller readiness vocabulary: pending or scanning is PROCESSING, an admissible unlinked upload is READY, and a linked file is ATTACHED. Other states are UNAVAILABLE; route withholding can refuse access before returning a projection. These are distinct from a malware scan verdict: READY can include an upload whose field does not require scanning.
The metadata may include a URL or thumbnail URL when one exists, but callers must not reconstruct one from the storage handle. Standard controls and displays use the appropriate file/resource services and route builders. A successful metadata read is not a promise that every other file-access route has identical authority rules.
When the parent reference changes, reconciliation can change the old row’s attachment state without rewriting every record that displays the new file. This separation is what lets cleanup and scanning continue to reason about files that are no longer visible in a form.