Storage & delivery Available
S3-opslag
Where your files' bytes live is a choice you make once, at deploy time. There is no screen for it and no per-organisation setting: api and worker get the same variables and that is the whole story.
What you need
- Access to your stack's environment variables (Docker Compose, Portainer or Swarm) and the ability to redeploy api and worker.
- An S3-compatible bucket with endpoint, region, bucket name and a key pair. Hetzner Object Storage, MinIO, Scaleway and AWS all qualify.
- A backup that takes the database and the storage volume together; the volume stays necessary after the switch for as long as files still sit on it.
How to connect it
- 1 By default there is nothing to do:
SCHAKL_STORAGE_BACKEND=localwrites to the named volumestorage-data, mounted into api and worker atSCHAKL_STORAGE_PATH(/data/storageby default). - 2 For object storage, set on both of those services
SCHAKL_STORAGE_BACKEND=s3plusSCHAKL_STORAGE_S3_ENDPOINT,_REGION,_BUCKET,_ACCESS_KEY_IDand_SECRET_ACCESS_KEY. Set it on only one and the other keeps writing to the volume. - 3 Optional:
SCHAKL_STORAGE_S3_KEY_PREFIXto keep everything under one path, andSCHAKL_STORAGE_S3_FORCE_PATH_STYLE, which already defaults totrueand is what keeps MinIO happy. - 4 Keep the keys out of your stack definition with
SCHAKL_STORAGE_S3_ACCESS_KEY_ID_FILEandSCHAKL_STORAGE_S3_SECRET_ACCESS_KEY_FILE, pointing at a Docker secret. An empty or unreadable file refuses the boot and names the path. - 5 Redeploy api and worker and upload one file, for instance with Bestand uploaden (upload file) on a task. From then on new files land in the bucket.
- 6 Leave the volume in place and keep it in your backup for as long as files still come from it. Which ones those are is answerable: every file row in the API carries its own
backendandstorage_key.
The same bytes, one object
A file is addressed by the sha256 of its own content, per organisation. A forwarded price list, a re-cropped logo and the signature image under every received mail therefore cost one object between them instead of a new one each time. De-duplication never crosses organisations: the key is organisation plus backend plus hash, and every object sits under that one organisation's prefix. That is deliberate — terminating an organisation deletes its whole prefix, and shared bytes would take somebody else's files with it.
Deleting frees no space right away
Deleting a file removes its row; the bytes stay a while longer. A maintenance job in the worker runs nightly at 03:15 UTC, per organisation, and does two things.
- Fold: rows from before de-duplication, 500 per organisation per night by default (SCHAKL_STORAGE_FOLD_BATCH). A large installation catches up over a few nights rather than during the upgrade.
- Sweep: bytes that no file row points at any more, and only after they have been seen unreferenced for SCHAKL_STORAGE_BLOB_GRACE_HOURS (24 by default).
- Side effect: inside that window a file deleted by mistake is still recoverable without restoring a backup, though that is hand work in the database.
Switching is not a migration
The backend is recorded per file row, so switching to s3 applies to new uploads only. Rolling back is just as simple: remove the variables and new uploads go to the volume again. A file whose backend cannot be reached answers with an explicit message about the object-storage configuration rather than an empty download.
- Existing files keep serving from the volume, so the volume stays necessary.
- Changing bucket or prefix later strands the objects already written.
- Database and volume belong in the same backup: a restored database without its volume leaves file rows whose bytes are gone.
Who can reach a file
Uploading and deleting needs the files.file.write permission, which by default sits with admin and member and not with a client account. Fetching a file declares no permission of its own: any signed-in colleague may read their own organisation's files, but the rows are tenant-scoped, the reader's company horizon applies, and a client logo or an HR dossier document answers 404 rather than a 403 that would give away its existence. Only branding assets (logo, favicon, app icon) are reachable without signing in, because they appear on the login screen; the organisation is resolved strictly from the hostname there.
Permissions
Grant these to the role that manages this integration (Settings → Roles). Admin-only by default, and never to the client role.
-
files.file.writeUploading and deleting files, everywhere the app accepts them. Held by admin and member by default, not by a client account.
What it deliberately does not do
- No storage screen: nowhere in the app does it say how much space you use, there is no per-organisation quota and no billing hook.
- No per-organisation or per-client setting: the backend applies to the whole installation and is chosen in the environment of api and worker.
- Enabling it moves nothing. There is no migrate button: existing files stay where they are.
- De-duplication never crosses organisations. That saves less space, but it keeps the separation between organisations intact.
- Deleting frees no space right away: the bytes go with the nightly sweep, at least a day later.
- If you roll a release back, do not delete files until you have rolled forward again. The previous version removes the object unconditionally and takes shared copies with it.
- A hard 10 MB ceiling per file (<code>SCHAKL_UPLOAD_MAX_BYTES</code>) and a fixed list of accepted types (<code>SCHAKL_UPLOAD_ALLOWED_TYPES</code>). Both belong to the install rather than the organisation: switching S3 on does not raise them.
Where to find it Nowhere in the app: this is an operator's choice, set in the environment of api and worker. The files themselves you meet under Documenten (Documents) on a project, attachments on a task, a client's logo and Instellingen → Huisstijl (Settings → Branding).
Read the guide