How files are stored
Everything an admin uploads, from gallery photos to rehearsal tracks, is kept outside the database in file storage. This page explains how that storage is arranged and covers the default, the server's own disk. It is for whoever installs the server and decides where files live.
Two stores
The site keeps files in two separate stores, because they need different protection.
| Store | What goes in it | Who can fetch a file |
|---|---|---|
| Public | Gallery photos and videos, concert posters, past-event posters, the logo and any picture uploaded into Site Settings. | Anyone. These are on your public pages anyway. |
| Private | Documents shared with members, and rehearsal tracks. | Nobody directly. Only the site reads this store, and it sends a file on only after checking that the person asking is a logged-in member or an admin. |
The private store must never be given a public address, whatever provider you use. The site does not hand out links into it: a member's browser asks the site for a file, the site checks the login, and then passes the file through. It does so in pieces when asked, so audio can be skipped forwards and backwards and plays in Safari.
Keys, not addresses
Each stored file has a key: a folder, the time of upload, a random part and the original extension, such as gallery/1767225600000-3f9a…c2.jpg. The original file name is not part of it.
For gallery items, past-event posters uploaded on the Past Events page, member files and rehearsal tracks, the database records the key, and the address is worked out each time a page is served. Those keep working if you later change where public files are served from.
Concert posters, the logo and pictures in Site Settings are different: uploading one fills the field with an address, and that address is what is saved. With the default settings it is a path on your own site, such as /files/posters/1767225600000-3f9a…c2.jpg, which goes on working as long as the file is in the store. With PUBLIC_FILES_URL set it is a full address on that other domain, and it is not rewritten if you change the setting afterwards.
| Folder in the key | Store | Holds |
|---|---|---|
gallery/ | public | Gallery photos and videos |
posters/ | public | Concert posters |
past-events/ | public | Posters uploaded on the Past Events page |
images/ | public | The logo and other pictures uploaded into settings |
member-files/ | private | Documents shared with members |
rehearsal-tracks/ | private | Rehearsal tracks |
Choosing a provider
STORAGE_PROVIDER chooses where both stores are kept.
| Platform | STORAGE_PROVIDER | Where files go | Page |
|---|---|---|---|
| Node, Docker | local (the default) | A folder on the server's disk | this page |
| Node, Docker | s3 | Two buckets on any S3-compatible service | S3-compatible storage |
| Cloudflare Workers | unset, or anything but s3 | Two R2 buckets bound to the Worker | R2 on Cloudflare |
| Cloudflare Workers | s3 | Two buckets on any S3-compatible service | S3-compatible storage |
On Node the value must be written exactly, in lower case. Anything else stops the server at start:
Unknown STORAGE_PROVIDER "…". Use local or s3 (r2 is for Cloudflare).
Local disk is not available on Cloudflare, and the R2 bindings are not available on Node. A Node server can still use R2 through its S3 interface.
Use local when the site is one server with a disk you back up. Use s3 when the disk does not last (many platform services), when there is more than one copy of the site, or when you would rather the files were on a storage service.
Local disk
| Variable | What it does | Default |
|---|---|---|
STORAGE_PROVIDER | local keeps files on the server's disk. | local on Node |
STORAGE_LOCAL_DIR | The folder to keep them in. | ./data/storage |
Inside that folder the site makes public and private, and inside each a folder for every kind of file as it is first needed:
data/storage/
public/
gallery/
1767225600000-3f9a…c2.jpg
1767225600000-3f9a…c2.jpg.meta.json
posters/
images/
private/
member-files/
rehearsal-tracks/
Beside every file is a small .meta.json holding the file's type, which the site reads when it sends the file. If one goes missing the site falls back on the file's extension, so the pair should be kept together but losing a .meta.json is not fatal.
Where the folder is
A relative STORAGE_LOCAL_DIR is taken from the folder the server is started in, as with the SQLite file. Set an absolute path, or make sure the working directory is always the same.
In the Docker image, docker-compose.sqlite.yml sets STORAGE_LOCAL_DIR=/app/data/storage on the data volume. Without a volume on /app/data, every upload is lost when the container is replaced: Docker: HTTPS, ports and volumes.
Permissions
The account the site runs as needs to read and write the whole folder. Nobody else needs any access, and the private half holds things only members should see:
sudo chown -R choir:choir /opt/choirmaster/data/storage
sudo chmod -R u=rwX,go= /opt/choirmaster/data/storage
Here choir stands for your service account.
Do not point a web server at this folder to serve files directly. Pointing one at storage or at private would publish members' files without any login. The site serves public files itself at /files/.
Space and backups
Local storage has no limit of its own. It grows with what is uploaded, and a gallery of videos at up to 100 MB each fills a small disk quickly. Watch the free space.
The folder is not copied by anything in the site, including the safety copy made before an update, which covers the database only. Back it up together with the database: Back up a single-server site.
Moving between providers
There is no tool that copies files from one provider to another.
Because keys are the same whatever the provider, the files can in principle be copied across with the storage service's own tools, keeping each key as it is: everything under public/ into the public bucket and everything under private/ into the private one, leaving out the .meta.json files and setting each object's content type. That path follows from how the site is built, but it is not a documented procedure and was not tested for this guide. Try it on a copy first, and remember that concert posters and settings pictures are saved as addresses, as described above.
If something goes wrong
| What you see | Cause |
|---|---|
An upload in the admin panel says Upload failed, and the log has EACCES or ENOSPC | The service account cannot write to the folder, or the disk is full. |
| Pictures uploaded before a restart or a move are broken | A relative STORAGE_LOCAL_DIR with a different working directory, or in Docker no volume on /app/data. |
A file's address answers File not found | The file is not in the store: it was deleted there, or the store was changed without copying the files. |