All integrations

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. 1 By default there is nothing to do: SCHAKL_STORAGE_BACKEND=local writes to the named volume storage-data, mounted into api and worker at SCHAKL_STORAGE_PATH (/data/storage by default).
  2. 2 For object storage, set on both of those services SCHAKL_STORAGE_BACKEND=s3 plus SCHAKL_STORAGE_S3_ENDPOINT, _REGION, _BUCKET, _ACCESS_KEY_ID and _SECRET_ACCESS_KEY. Set it on only one and the other keeps writing to the volume.
  3. 3 Optional: SCHAKL_STORAGE_S3_KEY_PREFIX to keep everything under one path, and SCHAKL_STORAGE_S3_FORCE_PATH_STYLE, which already defaults to true and is what keeps MinIO happy.
  4. 4 Keep the keys out of your stack definition with SCHAKL_STORAGE_S3_ACCESS_KEY_ID_FILE and SCHAKL_STORAGE_S3_SECRET_ACCESS_KEY_FILE, pointing at a Docker secret. An empty or unreadable file refuses the boot and names the path.
  5. 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. 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 backend and storage_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.write Uploading 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

More in this category

Other integrations