Skip to main content
Wildo.ai Coming soon

Files and storage

Keep a durable record of every stored file

An internal file record tracks storage, ownership and lifecycle separately from the bytes and the parent record’s small file reference.

A brief's stored contents are paired with metadata for its name, owner and status.

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:

InformationInternal file rowPublic metadata projection
IdentityfileId: "507f1f77bcf86cd799439011"The same fileId.
ContentOriginal filename, MIME type and byte size.Display filename, MIME type and size.
Lifecyclestatus: FileStatus.LINKED after attachment.readiness: FileReadiness.ATTACHED.
Parent bindinglinkedResourceType, linkedResourceId, linkedFieldName.These binding fields are not copied into the metadata projection.
StoragestorageProvider and the persisted storageRef.The raw provider handle is not exposed.
OwnershipApplication, 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.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.