Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 06 · Media and files

Virtual files and where uploads are stored

The bytes of every uploaded file and image are kept as a virtual file: small ones in the database, all of them in a store on disk. Why virtual files resolve last as templates, and why a migration must move the store with the database.

Who this is for. Developers moving or restoring a site, and anyone who meets a template that is in neither the repository nor the framework. After this page you know where uploads are stored and what a migration must carry.

What a virtual file is

Every revision of a file or image keeps its bytes in a virtual file. It is the storage under every upload, not an optional feature: the ordinary upload path creates one for each revision, and there is no admin screen for them. A virtual file records the content's checksum (MD5), its type, its size and its virtual filename.

Where the bytes are

  • In the database, when the content is 10 KB (10,240 bytes) or less.
  • On disk, always. Every virtual file, whatever its size, is also written to the account's virtual-files store, a folder per account that lives outside the site's folder and is linked into the site as .virtual-files. Deleting a virtual file deletes its copy on disk too.

So the bytes of a file over 10 KB exist only in the store on disk; its database row holds the checksum, type and size but not the content. A platform worker removes virtual files that nothing uses any more.

Why virtual files resolve last as templates

Smarty and Twig both look for a template in three places, in this order: the site's templates/, the framework's templates/, then the virtual-files store. A template path that exists in the repository or the framework is always found there first. A stored file is used as a template only when nothing earlier has that relative path.

The order is deliberate. The repository and the framework are what a release controls, so they win; the store is a fallback that sites rarely use on purpose. When a template renders that is in neither the repository nor the framework, it came from the store.

The more common use is to read a stored file's content directly, without treating it as a template:

Template code
{* a stored HTML file in folder 42, printed as it is *}
{$stored = $_query->findOneBy('Files\Entities\Filename', ['folder' => 42, 'name' => 'rendered-page.html'])}
{if $stored}{$stored->getFile()->getActiveRevision()->getVirtualFile()->getContent()}{/if}
{# a stored HTML file in folder 42, printed as it is #}
{% set stored = _query.findOneBy('Files\\Entities\\Filename', {'folder': 42, 'name': 'rendered-page.html'}) %}
{% if stored %}{{ stored.getFile().getActiveRevision().getVirtualFile().getContent() }}{% endif %}

The same chain, from the active revision to the virtual file's content, prints an image read from a custom field inline, as shown on Reading custom fields.

Easy to forget in a migration

None of this is in the repository. A clone of the site's code carries its templates and built assets; its uploads live in the database and the virtual-files store, which is what holds the bytes of every upload over 10 KB (the small ones are in the database too).

  • Move the store together with the database, taken at the same moment.
  • After the move, open a large image and a large document, not only a small one.
A small file proves nothing

Files of 10 KB or less are stored in the database as well as on disk, so they survive a database-only copy and make it look complete. Check the migration with a file well over 10 KB.

Go deeper

Was this page helpful?

Your answer goes to the docs team.